diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/debugger/src/components/Editor/Preview | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/components/Editor/Preview')
6 files changed, 1010 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js b/devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js new file mode 100644 index 0000000000..624a78fb8b --- /dev/null +++ b/devtools/client/debugger/src/components/Editor/Preview/ExceptionPopup.js @@ -0,0 +1,164 @@ +/* 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/>. */ + +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "../../../utils/connect"; + +import Reps from "devtools/client/shared/components/reps/index"; +const { + REPS: { StringRep }, +} = Reps; + +import actions from "../../../actions"; + +import { getThreadContext } from "../../../selectors"; + +import AccessibleImage from "../../shared/AccessibleImage"; + +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const classnames = require("devtools/client/shared/classnames.js"); + +const POPUP_SELECTOR = ".preview-popup.exception-popup"; +const ANONYMOUS_FN_NAME = "<anonymous>"; + +// The exception popup works in two modes: +// a. when the stacktrace is closed the exception popup +// gets closed when the mouse leaves the popup. +// b. when the stacktrace is opened the exception popup +// gets closed only by clicking outside the popup. +class ExceptionPopup extends Component { + constructor(props) { + super(props); + this.state = { + isStacktraceExpanded: false, + }; + } + + static get propTypes() { + return { + clearPreview: PropTypes.func.isRequired, + cx: PropTypes.object.isRequired, + mouseout: PropTypes.func.isRequired, + selectSourceURL: PropTypes.func.isRequired, + exception: PropTypes.object.isRequired, + }; + } + + updateTopWindow() { + // The ChromeWindow is used when the stacktrace is expanded to capture all clicks + // outside the popup so the popup can be closed only by clicking outside of it. + if (this.topWindow) { + this.topWindow.removeEventListener( + "mousedown", + this.onTopWindowClick, + true + ); + this.topWindow = null; + } + this.topWindow = DevToolsUtils.getTopWindow(window.parent); + this.topWindow.addEventListener("mousedown", this.onTopWindowClick, true); + } + + onTopWindowClick = e => { + const { cx, clearPreview } = this.props; + + // When the stactrace is expaned the exception popup gets closed + // only by clicking ouside the popup. + if (!e.target.closest(POPUP_SELECTOR)) { + clearPreview(cx); + } + }; + + onExceptionMessageClick() { + const isStacktraceExpanded = this.state.isStacktraceExpanded; + + this.updateTopWindow(); + this.setState({ isStacktraceExpanded: !isStacktraceExpanded }); + } + + buildStackFrame(frame) { + const { cx, selectSourceURL } = this.props; + const { filename, lineNumber } = frame; + const functionName = frame.functionName || ANONYMOUS_FN_NAME; + + return ( + <div + className="frame" + onClick={() => selectSourceURL(cx, filename, { line: lineNumber })} + > + <span className="title">{functionName}</span> + <span className="location"> + <span className="filename">{filename}</span>: + <span className="line">{lineNumber}</span> + </span> + </div> + ); + } + + renderStacktrace(stacktrace) { + const isStacktraceExpanded = this.state.isStacktraceExpanded; + + if (stacktrace.length && isStacktraceExpanded) { + return ( + <div className="exception-stacktrace"> + {stacktrace.map(frame => this.buildStackFrame(frame))} + </div> + ); + } + return null; + } + + renderArrowIcon(stacktrace) { + if (stacktrace.length) { + return ( + <AccessibleImage + className={classnames("arrow", { + expanded: this.state.isStacktraceExpanded, + })} + /> + ); + } + return null; + } + + render() { + const { + exception: { stacktrace, errorMessage }, + mouseout, + } = this.props; + + return ( + <div + className="preview-popup exception-popup" + dir="ltr" + onMouseLeave={() => mouseout(true, this.state.isStacktraceExpanded)} + > + <div + className="exception-message" + onClick={() => this.onExceptionMessageClick()} + > + {this.renderArrowIcon(stacktrace)} + {StringRep.rep({ + object: errorMessage, + useQuotes: false, + className: "exception-text", + })} + </div> + {this.renderStacktrace(stacktrace)} + </div> + ); + } +} + +const mapStateToProps = state => ({ + cx: getThreadContext(state), +}); + +const mapDispatchToProps = { + selectSourceURL: actions.selectSourceURL, + clearPreview: actions.clearPreview, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ExceptionPopup); diff --git a/devtools/client/debugger/src/components/Editor/Preview/Popup.css b/devtools/client/debugger/src/components/Editor/Preview/Popup.css new file mode 100644 index 0000000000..3e578becf1 --- /dev/null +++ b/devtools/client/debugger/src/components/Editor/Preview/Popup.css @@ -0,0 +1,209 @@ +/* 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/>. */ + +.popover .preview-popup { + background: var(--theme-body-background); + width: 350px; + border: 1px solid var(--theme-splitter-color); + padding: 10px; + height: auto; + overflow: auto; + box-shadow: 1px 2px 3px var(--popup-shadow-color); +} + +.preview-popup .tree { + /* Setting a fixed line height to avoid issues in custom formatters changing + * the line height like the CLJS DevTools */ + line-height: 15px; +} + +.gap svg { + pointer-events: none; +} + +.gap polygon { + pointer-events: auto; +} + +.theme-dark .popover .preview-popup { + box-shadow: 1px 2px 3px var(--popup-shadow-color); +} + +.popover .preview-popup .header-container { + width: 100%; + line-height: 15px; + display: flex; + flex-direction: row; + margin-bottom: 5px; +} + +.popover .preview-popup .logo { + width: 20px; + margin-right: 5px; +} + +.popover .preview-popup .header-container h3 { + margin: 0; + margin-bottom: 5px; + font-weight: normal; + font-size: 14px; + line-height: 20px; + margin-left: 4px; +} + +.popover .preview-popup .header .link { + align-self: flex-end; + color: var(--theme-highlight-blue); + text-decoration: underline; +} + +.popover .preview-popup .object-node { + padding-inline-start: 0px; +} + +.preview-token:hover { + cursor: default; +} + +.preview-token, +.debug-expression.preview-token { + background-color: var(--theme-highlight-yellow); +} + +.theme-dark .preview-token, +.theme-dark .debug-expression.preview-token { + background-color: #743884; +} + +.theme-dark .cm-s-mozilla .preview-token, +.theme-dark .cm-s-mozilla .debug-expression.preview-token { + color: #e7ebee; +} + +.popover .preview-popup .function-signature { + padding-top: 10px; +} + +.theme-dark .popover .preview-popup { + border-color: var(--theme-body-color); +} + +.tooltip { + position: fixed; + z-index: 100; +} + +.tooltip .preview-popup { + background: var(--theme-toolbar-background); + max-width: inherit; + border: 1px solid var(--theme-splitter-color); + box-shadow: 1px 2px 4px 1px var(--theme-toolbar-background-alt); + padding: 5px; + height: auto; + min-height: inherit; + max-height: 200px; + overflow: auto; +} + +.theme-dark .tooltip .preview-popup { + border-color: var(--theme-body-color); +} + +.tooltip .gap { + height: 4px; + padding-top: 0px; +} + +.add-to-expression-bar { + border: 1px solid var(--theme-splitter-color); + border-top: none; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-size: 14px; + line-height: 30px; + background: var(--theme-toolbar-background); + color: var(--theme-text-color-inactive); + padding: 0 4px; +} + +.add-to-expression-bar .prompt { + width: 1em; +} + +.add-to-expression-bar .expression-to-save-label { + width: calc(100% - 4em); +} + +.add-to-expression-bar .expression-to-save-button { + font-size: 14px; + color: var(--theme-comment); +} + +/* Exception popup */ +.exception-popup .exception-text { + color: var(--red-70); +} + +.theme-dark .exception-popup .exception-text { + color: var(--red-20); +} + +.exception-popup .exception-message { + display: flex; + align-items: center; +} + +.exception-message .arrow { + margin-inline-end: 4px; +} + +.exception-popup .exception-stacktrace { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 8px; + padding-inline: 2px 3px; + line-height: var(--theme-code-line-height); +} + +.exception-stacktrace .frame { + display: contents; + cursor: pointer; +} + +.exception-stacktrace .title { + grid-column: 1/2; + color: var(--grey-90); +} + +.theme-dark .exception-stacktrace .title { + color: white; +} + +.exception-stacktrace .location { + grid-column: -1/-2; + color: var(--theme-highlight-purple); + direction: rtl; + text-align: end; + white-space: nowrap; + /* Force the location to be on one line and crop at start if wider then max-width */ + overflow: hidden; + text-overflow: ellipsis; + max-width: 350px; +} + +.theme-dark .exception-stacktrace .location { + color: var(--blue-40); +} + +.exception-stacktrace .line { + color: var(--theme-highlight-blue); +} + +.theme-dark .exception-stacktrace .line { + color: hsl(210, 40%, 60%); +} diff --git a/devtools/client/debugger/src/components/Editor/Preview/Popup.js b/devtools/client/debugger/src/components/Editor/Preview/Popup.js new file mode 100644 index 0000000000..3097d3c945 --- /dev/null +++ b/devtools/client/debugger/src/components/Editor/Preview/Popup.js @@ -0,0 +1,382 @@ +/* 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/>. */ + +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "../../../utils/connect"; + +import Reps from "devtools/client/shared/components/reps/index"; +const { + REPS: { Rep }, + MODE, + objectInspector, +} = Reps; + +const { ObjectInspector, utils } = objectInspector; + +const { + node: { nodeIsPrimitive, nodeIsFunction, nodeIsObject }, +} = utils; + +import ExceptionPopup from "./ExceptionPopup"; + +import actions from "../../../actions"; +import { getThreadContext } from "../../../selectors"; +import Popover from "../../shared/Popover"; +import PreviewFunction from "../../shared/PreviewFunction"; + +import "./Popup.css"; + +export class Popup extends Component { + constructor(props) { + super(props); + } + + static get propTypes() { + return { + clearPreview: PropTypes.func.isRequired, + cx: PropTypes.object.isRequired, + editorRef: PropTypes.object.isRequired, + highlightDomElement: PropTypes.func.isRequired, + openElementInInspector: PropTypes.func.isRequired, + openLink: PropTypes.func.isRequired, + preview: PropTypes.object.isRequired, + selectSourceURL: PropTypes.func.isRequired, + unHighlightDomElement: PropTypes.func.isRequired, + }; + } + + componentDidMount() { + this.addHighlightToToken(); + } + + componentWillUnmount() { + this.removeHighlightFromToken(); + } + + addHighlightToToken() { + const { target } = this.props.preview; + if (target) { + target.classList.add("preview-token"); + addHighlightToTargetSiblings(target, this.props); + } + } + + removeHighlightFromToken() { + const { target } = this.props.preview; + if (target) { + target.classList.remove("preview-token"); + removeHighlightForTargetSiblings(target); + } + } + + calculateMaxHeight = () => { + const { editorRef } = this.props; + if (!editorRef) { + return "auto"; + } + + const { height, top } = editorRef.getBoundingClientRect(); + const maxHeight = height + top; + if (maxHeight < 250) { + return maxHeight; + } + + return 250; + }; + + createElement(element) { + return document.createElement(element); + } + + renderFunctionPreview() { + const { + cx, + selectSourceURL, + preview: { resultGrip }, + } = this.props; + + if (!resultGrip) { + return null; + } + + const { location } = resultGrip; + + return ( + <div + className="preview-popup" + onClick={() => + location && + selectSourceURL(cx, location.url, { + line: location.line, + }) + } + > + <PreviewFunction func={resultGrip} /> + </div> + ); + } + + renderObjectPreview() { + const { + preview: { root, properties }, + openLink, + openElementInInspector, + highlightDomElement, + unHighlightDomElement, + } = this.props; + + const usesCustomFormatter = + root?.contents?.value?.useCustomFormatter ?? false; + + if (!properties.length) { + return ( + <div className="preview-popup"> + <span className="label">{L10N.getStr("preview.noProperties")}</span> + </div> + ); + } + + const roots = usesCustomFormatter ? [root] : properties; + + return ( + <div + className="preview-popup" + style={{ maxHeight: this.calculateMaxHeight() }} + > + <ObjectInspector + roots={roots} + autoExpandDepth={0} + autoReleaseObjectActors={false} + mode={usesCustomFormatter ? MODE.LONG : null} + disableWrap={true} + focusable={false} + openLink={openLink} + createElement={this.createElement} + onDOMNodeClick={grip => openElementInInspector(grip)} + onInspectIconClick={grip => openElementInInspector(grip)} + onDOMNodeMouseOver={grip => highlightDomElement(grip)} + onDOMNodeMouseOut={grip => unHighlightDomElement(grip)} + mayUseCustomFormatter={true} + /> + </div> + ); + } + + renderSimplePreview() { + const { + openLink, + preview: { resultGrip }, + } = this.props; + return ( + <div className="preview-popup"> + {Rep({ + object: resultGrip, + mode: MODE.LONG, + openLink, + })} + </div> + ); + } + + renderExceptionPreview(exception) { + return ( + <ExceptionPopup + exception={exception} + mouseout={this.onMouseOutException} + /> + ); + } + + renderPreview() { + // We don't have to check and + // return on `false`, `""`, `0`, `undefined` etc, + // these falsy simple typed value because we want to + // do `renderSimplePreview` on these values below. + const { + preview: { root, exception }, + } = this.props; + + if (nodeIsFunction(root)) { + return this.renderFunctionPreview(); + } + + if (nodeIsObject(root)) { + return <div>{this.renderObjectPreview()}</div>; + } + + if (exception) { + return this.renderExceptionPreview(exception); + } + + return this.renderSimplePreview(); + } + + getPreviewType() { + const { + preview: { root, properties, exception }, + } = this.props; + if ( + exception || + nodeIsPrimitive(root) || + nodeIsFunction(root) || + !Array.isArray(properties) || + properties.length === 0 + ) { + return "tooltip"; + } + + return "popover"; + } + + onMouseOut = () => { + const { clearPreview, cx } = this.props; + + clearPreview(cx); + }; + + onMouseOutException = (shouldClearOnMouseout, isExceptionStactraceOpen) => { + // onMouseOutException can be called: + // a. when the mouse leaves Popover element + // b. when the mouse leaves ExceptionPopup element + // We want to prevent closing the popup when the stacktrace + // is expanded and the mouse leaves either the Popover element + // or the ExceptionPopup element. + const { clearPreview, cx } = this.props; + + if (shouldClearOnMouseout) { + this.isExceptionStactraceOpen = isExceptionStactraceOpen; + } + + if (!this.isExceptionStactraceOpen) { + clearPreview(cx); + } + }; + + render() { + const { + preview: { cursorPos, resultGrip, exception }, + editorRef, + } = this.props; + + if ( + !exception && + (typeof resultGrip == "undefined" || resultGrip?.optimizedOut) + ) { + return null; + } + + const type = this.getPreviewType(); + return ( + <Popover + targetPosition={cursorPos} + type={type} + editorRef={editorRef} + target={this.props.preview.target} + mouseout={exception ? this.onMouseOutException : this.onMouseOut} + > + {this.renderPreview()} + </Popover> + ); + } +} + +export function addHighlightToTargetSiblings(target, props) { + // This function searches for related tokens that should also be highlighted when previewed. + // Here is the process: + // It conducts a search on the target's next siblings and then another search for the previous siblings. + // If a sibling is not an element node (nodeType === 1), the highlight is not added and the search is short-circuited. + // If the element sibling is the same token type as the target, and is also found in the preview expression, the highlight class is added. + + const tokenType = target.classList.item(0); + const previewExpression = props.preview.expression; + + if ( + tokenType && + previewExpression && + target.innerHTML !== previewExpression + ) { + let nextSibling = target.nextSibling; + let nextElementSibling = target.nextElementSibling; + + // Note: Declaring previous/next ELEMENT siblings as well because + // properties like innerHTML can't be checked on nextSibling + // without creating a flow error even if the node is an element type. + while ( + nextSibling && + nextElementSibling && + nextSibling.nodeType === 1 && + nextElementSibling.className.includes(tokenType) && + previewExpression.includes(nextElementSibling.innerHTML) + ) { + // All checks passed, add highlight and continue the search. + nextElementSibling.classList.add("preview-token"); + + nextSibling = nextSibling.nextSibling; + nextElementSibling = nextElementSibling.nextElementSibling; + } + + let previousSibling = target.previousSibling; + let previousElementSibling = target.previousElementSibling; + + while ( + previousSibling && + previousElementSibling && + previousSibling.nodeType === 1 && + previousElementSibling.className.includes(tokenType) && + previewExpression.includes(previousElementSibling.innerHTML) + ) { + // All checks passed, add highlight and continue the search. + previousElementSibling.classList.add("preview-token"); + + previousSibling = previousSibling.previousSibling; + previousElementSibling = previousElementSibling.previousElementSibling; + } + } +} + +export function removeHighlightForTargetSiblings(target) { + // Look at target's previous and next token siblings. + // If they also have the highlight class 'preview-token', + // remove that class. + let nextSibling = target.nextElementSibling; + while (nextSibling && nextSibling.className.includes("preview-token")) { + nextSibling.classList.remove("preview-token"); + nextSibling = nextSibling.nextElementSibling; + } + let previousSibling = target.previousElementSibling; + while ( + previousSibling && + previousSibling.className.includes("preview-token") + ) { + previousSibling.classList.remove("preview-token"); + previousSibling = previousSibling.previousElementSibling; + } +} + +const mapStateToProps = state => ({ + cx: getThreadContext(state), +}); + +const { + addExpression, + selectSourceURL, + openLink, + openElementInInspectorCommand, + highlightDomElement, + unHighlightDomElement, + clearPreview, +} = actions; + +const mapDispatchToProps = { + addExpression, + selectSourceURL, + openLink, + openElementInInspector: openElementInInspectorCommand, + highlightDomElement, + unHighlightDomElement, + clearPreview, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Popup); diff --git a/devtools/client/debugger/src/components/Editor/Preview/index.js b/devtools/client/debugger/src/components/Editor/Preview/index.js new file mode 100644 index 0000000000..0e2c70c557 --- /dev/null +++ b/devtools/client/debugger/src/components/Editor/Preview/index.js @@ -0,0 +1,136 @@ +/* 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/>. */ + +import PropTypes from "prop-types"; +import React, { PureComponent } from "react"; +import { connect } from "../../../utils/connect"; + +import Popup from "./Popup"; + +import { + getPreview, + getThreadContext, + getCurrentThread, + getHighlightedCalls, + getIsCurrentThreadPaused, +} from "../../../selectors"; +import actions from "../../../actions"; + +const EXCEPTION_MARKER = "mark-text-exception"; + +class Preview extends PureComponent { + target = null; + constructor(props) { + super(props); + this.state = { selecting: false }; + } + + static get propTypes() { + return { + clearPreview: PropTypes.func.isRequired, + cx: PropTypes.object.isRequired, + editor: PropTypes.object.isRequired, + editorRef: PropTypes.object.isRequired, + highlightedCalls: PropTypes.array, + isPaused: PropTypes.bool.isRequired, + preview: PropTypes.object, + setExceptionPreview: PropTypes.func.isRequired, + updatePreview: PropTypes.func.isRequired, + }; + } + + componentDidMount() { + this.updateListeners(); + } + + componentWillUnmount() { + const { codeMirror } = this.props.editor; + const codeMirrorWrapper = codeMirror.getWrapperElement(); + + codeMirror.off("tokenenter", this.onTokenEnter); + codeMirror.off("scroll", this.onScroll); + codeMirrorWrapper.removeEventListener("mouseup", this.onMouseUp); + codeMirrorWrapper.removeEventListener("mousedown", this.onMouseDown); + } + + updateListeners(prevProps) { + const { codeMirror } = this.props.editor; + const codeMirrorWrapper = codeMirror.getWrapperElement(); + codeMirror.on("tokenenter", this.onTokenEnter); + codeMirror.on("scroll", this.onScroll); + codeMirrorWrapper.addEventListener("mouseup", this.onMouseUp); + codeMirrorWrapper.addEventListener("mousedown", this.onMouseDown); + } + + onTokenEnter = ({ target, tokenPos }) => { + const { cx, editor, updatePreview, highlightedCalls, setExceptionPreview } = + this.props; + + const isTargetException = target.classList.contains(EXCEPTION_MARKER); + + if (isTargetException) { + setExceptionPreview(cx, target, tokenPos, editor.codeMirror); + return; + } + + if ( + this.props.isPaused && + !this.state.selecting && + highlightedCalls === null && + !isTargetException + ) { + updatePreview(cx, target, tokenPos, editor.codeMirror); + } + }; + + onMouseUp = () => { + if (this.props.isPaused) { + this.setState({ selecting: false }); + } + }; + + onMouseDown = () => { + if (this.props.isPaused) { + this.setState({ selecting: true }); + } + }; + + onScroll = () => { + if (this.props.isPaused) { + this.props.clearPreview(this.props.cx); + } + }; + + render() { + const { preview } = this.props; + if (!preview || this.state.selecting) { + return null; + } + + return ( + <Popup + preview={preview} + editor={this.props.editor} + editorRef={this.props.editorRef} + /> + ); + } +} + +const mapStateToProps = state => { + const thread = getCurrentThread(state); + return { + highlightedCalls: getHighlightedCalls(state, thread), + cx: getThreadContext(state), + preview: getPreview(state), + isPaused: getIsCurrentThreadPaused(state), + }; +}; + +export default connect(mapStateToProps, { + clearPreview: actions.clearPreview, + addExpression: actions.addExpression, + updatePreview: actions.updatePreview, + setExceptionPreview: actions.setExceptionPreview, +})(Preview); diff --git a/devtools/client/debugger/src/components/Editor/Preview/moz.build b/devtools/client/debugger/src/components/Editor/Preview/moz.build new file mode 100644 index 0000000000..362faadc42 --- /dev/null +++ b/devtools/client/debugger/src/components/Editor/Preview/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# 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/. + +DIRS += [] + +CompiledModules( + "ExceptionPopup.js", + "index.js", + "Popup.js", +) diff --git a/devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js b/devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js new file mode 100644 index 0000000000..8c58fe9c63 --- /dev/null +++ b/devtools/client/debugger/src/components/Editor/Preview/tests/Popup.spec.js @@ -0,0 +1,107 @@ +/* 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/>. */ + +import { + addHighlightToTargetSiblings, + removeHighlightForTargetSiblings, +} from "../Popup"; + +describe("addHighlightToTargetSiblings", () => { + it("should add preview highlight class to related target siblings", async () => { + const div = document.createElement("div"); + const divChildren = ["a", "divided", "token"]; + divChildren.forEach(function (span) { + const child = document.createElement("span"); + const text = document.createTextNode(span); + child.appendChild(text); + child.classList.add("cm-property"); + div.appendChild(child); + }); + + const target = div.children[1]; + const props = { + preview: { + expression: "adividedtoken", + }, + }; + + addHighlightToTargetSiblings(target, props); + + const previous = target.previousElementSibling; + if (previous && previous.className) { + expect(previous.className.includes("preview-token")).toEqual(true); + } + + const next = target.nextElementSibling; + if (next && next.className) { + expect(next.className.includes("preview-token")).toEqual(true); + } + }); + + it("should not add preview highlight class to target's related siblings after non-element nodes", () => { + const div = document.createElement("div"); + + const elementBeforePeriod = document.createElement("span"); + elementBeforePeriod.innerHTML = "object"; + elementBeforePeriod.classList.add("cm-property"); + div.appendChild(elementBeforePeriod); + + const period = document.createTextNode("."); + div.appendChild(period); + + const target = document.createElement("span"); + target.innerHTML = "property"; + target.classList.add("cm-property"); + div.appendChild(target); + + const anotherPeriod = document.createTextNode("."); + div.appendChild(anotherPeriod); + + const elementAfterPeriod = document.createElement("span"); + elementAfterPeriod.innerHTML = "anotherProperty"; + elementAfterPeriod.classList.add("cm-property"); + div.appendChild(elementAfterPeriod); + + const props = { + preview: { + expression: "object.property.anotherproperty", + }, + }; + addHighlightToTargetSiblings(target, props); + + expect(elementBeforePeriod.className.includes("preview-token")).toEqual( + false + ); + expect(elementAfterPeriod.className.includes("preview-token")).toEqual( + false + ); + }); +}); + +describe("removeHighlightForTargetSiblings", () => { + it("should remove preview highlight class from target's related siblings", async () => { + const div = document.createElement("div"); + const divChildren = ["a", "divided", "token"]; + divChildren.forEach(function (span) { + const child = document.createElement("span"); + const text = document.createTextNode(span); + child.appendChild(text); + child.classList.add("preview-token"); + div.appendChild(child); + }); + const target = div.children[1]; + + removeHighlightForTargetSiblings(target); + + const previous = target.previousElementSibling; + if (previous && previous.className) { + expect(previous.className.includes("preview-token")).toEqual(false); + } + + const next = target.nextElementSibling; + if (next && next.className) { + expect(next.className.includes("preview-token")).toEqual(false); + } + }); +}); |