chore: use codemirror in the on-hover locator editor (#28090)

This commit is contained in:
Pavel Feldman 2023-11-10 22:00:28 -08:00 committed by GitHub
parent fae5dd898a
commit 1b3349d091
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 68 deletions

20
package-lock.json generated
View file

@ -2283,10 +2283,10 @@
"mimic-response": "^1.0.0" "mimic-response": "^1.0.0"
} }
}, },
"node_modules/codemirror": { "node_modules/codemirror-shadow-1": {
"version": "5.65.9", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", "resolved": "https://registry.npmjs.org/codemirror-shadow-1/-/codemirror-shadow-1-0.0.1.tgz",
"integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==" "integrity": "sha512-kD3OZpCCHr3LHRKfbGx5IogHTWq4Uo9jH2bXPVa7/n6ppkgI66rx4tniQY1BpqWp/JNhQmQsXhQoaZ1TH6t0xQ=="
}, },
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
@ -7310,7 +7310,7 @@
"packages/web": { "packages/web": {
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"codemirror": "^5.65.9", "codemirror-shadow-1": "0.0.1",
"xterm": "^5.1.0", "xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0" "xterm-addon-fit": "^0.7.0"
} }
@ -8962,10 +8962,10 @@
"mimic-response": "^1.0.0" "mimic-response": "^1.0.0"
} }
}, },
"codemirror": { "codemirror-shadow-1": {
"version": "5.65.9", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", "resolved": "https://registry.npmjs.org/codemirror-shadow-1/-/codemirror-shadow-1-0.0.1.tgz",
"integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw==" "integrity": "sha512-kD3OZpCCHr3LHRKfbGx5IogHTWq4Uo9jH2bXPVa7/n6ppkgI66rx4tniQY1BpqWp/JNhQmQsXhQoaZ1TH6t0xQ=="
}, },
"color-convert": { "color-convert": {
"version": "1.9.3", "version": "1.9.3",
@ -11862,7 +11862,7 @@
"web": { "web": {
"version": "file:packages/web", "version": "file:packages/web",
"requires": { "requires": {
"codemirror": "^5.65.9", "codemirror-shadow-1": "0.0.1",
"xterm": "^5.1.0", "xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0" "xterm-addon-fit": "^0.7.0"
} }

View file

@ -10,7 +10,7 @@ This project incorporates components from the projects listed below. The origina
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match) - balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion) - brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32) - buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
- codemirror@5.65.9 (https://github.com/codemirror/CodeMirror) - codemirror-shadow-1@0.0.1 (https://github.com/codemirror/CodeMirror)
- colors@1.4.0 (https://github.com/Marak/colors.js) - colors@1.4.0 (https://github.com/Marak/colors.js)
- commander@8.3.0 (https://github.com/tj/commander.js) - commander@8.3.0 (https://github.com/tj/commander.js)
- concat-map@0.0.1 (https://github.com/substack/node-concat-map) - concat-map@0.0.1 (https://github.com/substack/node-concat-map)
@ -326,11 +326,11 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEAL
========================================= =========================================
END OF buffer-crc32@0.2.13 AND INFORMATION END OF buffer-crc32@0.2.13 AND INFORMATION
%% codemirror@5.65.9 NOTICES AND INFORMATION BEGIN HERE %% codemirror-shadow-1@0.0.1 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
MIT License MIT License
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others Copyright (C) 2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -350,7 +350,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
========================================= =========================================
END OF codemirror@5.65.9 AND INFORMATION END OF codemirror-shadow-1@0.0.1 AND INFORMATION
%% colors@1.4.0 NOTICES AND INFORMATION BEGIN HERE %% colors@1.4.0 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================

View file

@ -44,8 +44,9 @@ x-pw-dialog {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
min-width: 500px; width: 500px;
min-height: 200px; height: 200px;
z-index: 10;
} }
x-pw-dialog-body { x-pw-dialog-body {
@ -54,6 +55,17 @@ x-pw-dialog-body {
flex: auto; flex: auto;
} }
x-pw-dialog-body label {
margin: 10px;
display: flex;
align-items: center;
cursor: pointer;
}
x-pw-dialog-body input {
cursor: pointer;
}
x-pw-highlight { x-pw-highlight {
position: absolute; position: absolute;
top: 0; top: 0;
@ -205,27 +217,17 @@ x-pw-overlay x-pw-tool-item {
margin: 2px; margin: 2px;
} }
input.locator-editor {
display: flex;
padding: 10px;
flex: none;
border: none;
border-bottom: 1px solid #dddddd;
}
input.locator-editor:focus,
textarea.text-editor:focus {
outline: none;
}
textarea.text-editor { textarea.text-editor {
font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif;
flex: auto; flex: auto;
border: none; border: none;
padding: 10px; margin: 10px;
color: #333; color: #333;
} }
textarea.text-editor:focus {
outline: none;
}
x-div { x-div {
display: block; display: block;
@ -242,3 +244,16 @@ x-spacer {
*[hidden] { *[hidden] {
display: none !important; display: none !important;
} }
x-locator-editor {
flex: none;
width: 100%;
height: 60px;
padding: 4px;
border-bottom: 1px solid #dddddd;
}
.CodeMirror {
width: 100% !important;
height: 100% !important;
}

View file

@ -60,7 +60,7 @@ export class Highlight {
this._glassPaneElement.style.pointerEvents = 'none'; this._glassPaneElement.style.pointerEvents = 'none';
this._glassPaneElement.style.display = 'flex'; this._glassPaneElement.style.display = 'flex';
this._glassPaneElement.style.backgroundColor = 'transparent'; this._glassPaneElement.style.backgroundColor = 'transparent';
for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mousemove', 'mouseleave', 'focus', 'scroll']) { for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mouseleave', 'focus', 'scroll']) {
this._glassPaneElement.addEventListener(eventName, e => { this._glassPaneElement.addEventListener(eventName, e => {
e.stopPropagation(); e.stopPropagation();
e.stopImmediatePropagation(); e.stopImmediatePropagation();

View file

@ -23,10 +23,28 @@ import { Highlight, type HighlightOptions } from '../injected/highlight';
import { isInsideScope } from './domUtils'; import { isInsideScope } from './domUtils';
import { elementText } from './selectorUtils'; import { elementText } from './selectorUtils';
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser'; import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import { parseSelector } from '@isomorphic/selectorParser'; import { parseSelector } from '@isomorphic/selectorParser';
import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; import { normalizeWhiteSpace } from '@isomorphic/stringUtils';
// @ts-ignore @no-check-deps
import CodeMirrorImpl from 'codemirror-shadow-1';
import type CodeMirrorType from 'codemirror';
// @no-check-deps
import codemirrorCSS from 'codemirror-shadow-1/lib/codemirror.css?inline';
// @no-check-deps
import 'codemirror-shadow-1/mode/css/css';
// @no-check-deps
import 'codemirror-shadow-1/mode/htmlmixed/htmlmixed';
// @no-check-deps
import 'codemirror-shadow-1/mode/javascript/javascript';
// @no-check-deps
import 'codemirror-shadow-1/mode/python/python';
// @no-check-deps
import 'codemirror-shadow-1/mode/clike/clike';
const CodeMirror = CodeMirrorImpl as typeof CodeMirrorType;
interface RecorderDelegate { interface RecorderDelegate {
performAction?(action: actions.Action): Promise<void>; performAction?(action: actions.Action): Promise<void>;
recordAction?(action: actions.Action): Promise<void>; recordAction?(action: actions.Action): Promise<void>;
@ -507,7 +525,7 @@ class TextAssertionTool implements RecorderTool {
selector, selector,
signals: [], signals: [],
// Interestingly, inputElement.checked is reversed inside this event handler. // Interestingly, inputElement.checked is reversed inside this event handler.
checked: (target as HTMLInputElement).checked, checked: !(target as HTMLInputElement).checked,
}; };
} else { } else {
return { return {
@ -576,12 +594,21 @@ class TextAssertionTool implements RecorderTool {
this._dialogElement.appendChild(toolbarElement); this._dialogElement.appendChild(toolbarElement);
const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); const bodyElement = this._recorder.document.createElement('x-pw-dialog-body');
const locatorElement = this._recorder.document.createElement('input'); const cmStyle = this._recorder.document.createElement('style');
locatorElement.classList.add('locator-editor'); const cmElement = this._recorder.document.createElement('x-locator-editor');
locatorElement.value = asLocator(this._recorder.state.language, this._action.selector); cmStyle.textContent = codemirrorCSS;
locatorElement.addEventListener('input', () => { bodyElement.appendChild(cmStyle);
bodyElement.appendChild(cmElement);
const cm = CodeMirror(cmElement, {
value: asLocator(this._recorder.state.language, this._action.selector),
mode: cmModeForLanguage(this._recorder.state.language),
readOnly: false,
lineNumbers: false,
lineWrapping: true,
});
cm.on('change', () => {
if (this._action) { if (this._action) {
const selector = locatorOrSelectorAsSelector(this._recorder.state.language, locatorElement.value, this._recorder.state.testIdAttributeName); const selector = locatorOrSelectorAsSelector(this._recorder.state.language, cm.getValue(), this._recorder.state.testIdAttributeName);
const model: HighlightModel = { const model: HighlightModel = {
selector, selector,
elements: this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document), elements: this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document),
@ -590,27 +617,46 @@ class TextAssertionTool implements RecorderTool {
this._recorder.updateHighlight(model, true); this._recorder.updateHighlight(model, true);
} }
}); });
const textElement = this._recorder.document.createElement('textarea');
textElement.value = this._renderValue(this._action);
textElement.classList.add('text-editor');
textElement.addEventListener('input', () => { let elementToFocus: HTMLElement | null = null;
if (this._action?.name === 'assertText') if (this._action.name !== 'assertChecked') {
this._action.text = normalizeWhiteSpace(elementText(new Map(), textElement).full); const textElement = this._recorder.document.createElement('textarea');
if (this._action?.name === 'assertChecked') textElement.setAttribute('spellcheck', 'false');
this._action.checked = textElement.value === 'true'; textElement.value = this._renderValue(this._action);
if (this._action?.name === 'assertValue') textElement.classList.add('text-editor');
this._action.value = textElement.value;
}); textElement.addEventListener('input', () => {
if (this._action?.name === 'assertText')
this._action.text = normalizeWhiteSpace(elementText(new Map(), textElement).full);
if (this._action?.name === 'assertChecked')
this._action.checked = textElement.value === 'true';
if (this._action?.name === 'assertValue')
this._action.value = textElement.value;
});
bodyElement.appendChild(textElement);
elementToFocus = textElement;
} else {
const labelElement = this._recorder.document.createElement('label');
labelElement.textContent = 'Value:';
const checkboxElement = this._recorder.document.createElement('input');
labelElement.appendChild(checkboxElement);
checkboxElement.type = 'checkbox';
checkboxElement.checked = this._action.checked;
checkboxElement.addEventListener('change', () => {
if (this._action?.name === 'assertChecked')
this._action.checked = checkboxElement.checked;
});
bodyElement.appendChild(labelElement);
elementToFocus = labelElement;
}
bodyElement.appendChild(locatorElement);
bodyElement.appendChild(textElement);
this._dialogElement.appendChild(bodyElement); this._dialogElement.appendChild(bodyElement);
this._recorder.highlight.appendChild(this._dialogElement); this._recorder.highlight.appendChild(this._dialogElement);
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement); const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement);
this._dialogElement.style.top = position.anchorTop + 'px'; this._dialogElement.style.top = position.anchorTop + 'px';
this._dialogElement.style.left = position.anchorLeft + 'px'; this._dialogElement.style.left = position.anchorLeft + 'px';
textElement.focus(); elementToFocus?.focus();
cm.refresh();
} }
private _createLabel(action: actions.AssertAction) { private _createLabel(action: actions.AssertAction) {
@ -1131,4 +1177,14 @@ export class PollingRecorder implements RecorderDelegate {
} }
} }
function cmModeForLanguage(language: Language): string {
if (language === 'python')
return 'python';
if (language === 'java')
return 'text/x-java';
if (language === 'csharp')
return 'text/x-csharp';
return 'javascript';
}
export default PollingRecorder; export default PollingRecorder;

View file

@ -99,7 +99,7 @@ This project incorporates components from the projects listed below. The origina
- chalk@4.1.2 (https://github.com/chalk/chalk) - chalk@4.1.2 (https://github.com/chalk/chalk)
- chokidar@3.5.3 (https://github.com/paulmillr/chokidar) - chokidar@3.5.3 (https://github.com/paulmillr/chokidar)
- ci-info@3.8.0 (https://github.com/watson/ci-info) - ci-info@3.8.0 (https://github.com/watson/ci-info)
- codemirror@5.65.9 (https://github.com/codemirror/CodeMirror) - codemirror-shadow-1@0.0.1 (https://github.com/codemirror/CodeMirror)
- color-convert@1.9.3 (https://github.com/Qix-/color-convert) - color-convert@1.9.3 (https://github.com/Qix-/color-convert)
- color-convert@2.0.1 (https://github.com/Qix-/color-convert) - color-convert@2.0.1 (https://github.com/Qix-/color-convert)
- color-name@1.1.3 (https://github.com/dfcreative/color-name) - color-name@1.1.3 (https://github.com/dfcreative/color-name)
@ -3156,11 +3156,11 @@ SOFTWARE.
========================================= =========================================
END OF ci-info@3.8.0 AND INFORMATION END OF ci-info@3.8.0 AND INFORMATION
%% codemirror@5.65.9 NOTICES AND INFORMATION BEGIN HERE %% codemirror-shadow-1@0.0.1 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
MIT License MIT License
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others Copyright (C) 2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -3180,7 +3180,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
========================================= =========================================
END OF codemirror@5.65.9 AND INFORMATION END OF codemirror-shadow-1@0.0.1 AND INFORMATION
%% color-convert@1.9.3 NOTICES AND INFORMATION BEGIN HERE %% color-convert@1.9.3 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================

View file

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"codemirror": "^5.65.9", "codemirror-shadow-1": "0.0.1",
"xterm": "^5.1.0", "xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0" "xterm-addon-fit": "^0.7.0"
} }

View file

@ -14,13 +14,15 @@
limitations under the License. limitations under the License.
*/ */
import codemirror from 'codemirror'; // @ts-ignore
import 'codemirror/lib/codemirror.css'; import codemirror from 'codemirror-shadow-1';
import 'codemirror/mode/css/css'; import type codemirrorType from 'codemirror';
import 'codemirror/mode/htmlmixed/htmlmixed'; import 'codemirror-shadow-1/lib/codemirror.css';
import 'codemirror/mode/javascript/javascript'; import 'codemirror-shadow-1/mode/css/css';
import 'codemirror/mode/python/python'; import 'codemirror-shadow-1/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/clike/clike'; import 'codemirror-shadow-1/mode/javascript/javascript';
import 'codemirror-shadow-1/mode/python/python';
import 'codemirror-shadow-1/mode/clike/clike';
export type CodeMirror = typeof codemirror; export type CodeMirror = typeof codemirrorType;
export default codemirror; export default codemirror;

View file

@ -79,7 +79,7 @@ async function innerCheckDeps(root) {
}); });
const sourceFiles = program.getSourceFiles(); const sourceFiles = program.getSourceFiles();
const errors = []; const errors = [];
sourceFiles.filter(x => !x.fileName.includes('node_modules')).map(x => visit(x, x.fileName)); sourceFiles.filter(x => !x.fileName.includes('node_modules')).map(x => visit(x, x.fileName, x.getFullText()));
if (errors.length) { if (errors.length) {
for (const error of errors) for (const error of errors)
@ -112,7 +112,7 @@ async function innerCheckDeps(root) {
return packageJSON; return packageJSON;
function visit(node, fileName) { function visit(node, fileName, text) {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
if (node.importClause) { if (node.importClause) {
if (node.importClause.isTypeOnly) if (node.importClause.isTypeOnly)
@ -151,6 +151,14 @@ async function innerCheckDeps(root) {
return; return;
} }
const fullStart = node.getFullStart();
const commentRanges = ts.getLeadingCommentRanges(text, fullStart);
for (const range of commentRanges || []) {
const comment = text.substring(range.pos, range.end);
if (comment.includes('@no-check-deps'))
return;
}
if (importName.startsWith('@')) if (importName.startsWith('@'))
deps.add(importName.split('/').slice(0, 2).join('/')); deps.add(importName.split('/').slice(0, 2).join('/'));
else else
@ -159,7 +167,7 @@ async function innerCheckDeps(root) {
if (!allowExternalImport(importName, packageJSON)) if (!allowExternalImport(importName, packageJSON))
errors.push(`Disallowed external dependency ${importName} from ${path.relative(root, fileName)}`); errors.push(`Disallowed external dependency ${importName} from ${path.relative(root, fileName)}`);
} }
ts.forEachChild(node, x => visit(x, fileName)); ts.forEachChild(node, x => visit(x, fileName, text));
} }
function calculateDeps(from) { function calculateDeps(from) {

View file

@ -59,7 +59,7 @@ This project incorporates components from the projects listed below. The origina
} }
} }
const packages = await checkDir('node_modules/codemirror'); const packages = await checkDir('node_modules/codemirror-shadow-1');
for (const [key, value] of Object.entries(packages)) { for (const [key, value] of Object.entries(packages)) {
if (value.licenseText) if (value.licenseText)
allPackages[key] = value; allPackages[key] = value;