Skip to content

Commit

Permalink
Merge branch 'dev' into CUC-514-dropdown-style
Browse files Browse the repository at this point in the history
  • Loading branch information
NessArt committed Dec 22, 2023
2 parents d7fbc9e + 8a85d0f commit 019fb9a
Show file tree
Hide file tree
Showing 9 changed files with 494 additions and 55 deletions.
21 changes: 16 additions & 5 deletions backend/features/step_definitions/stepdefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,17 @@ Then('So I will be navigated to the website: {string}', async function checkUrl(
await driver.sleep(100 + currentParameters.waitTime);
});

const resolveRegex = (rawString) => {
// undefined to empty string
rawString = !rawString ? '' : rawString;
const regex = /\{Regex:([^}]*(?:\{[^}]*\}[^}]*)*)(\})(?=\s|$)/g;
return rawString.replace(regex, '($1)');
};

// Search a textfield in the html code and assert it with a Text
Then('So I can see the text {string} in the textbox: {string}', async function checkForTextInField(expectedText, label) {
const resultString = resolveRegex(expectedText);

const world = this;

const identifiers = [`//*[@id='${label}']`, `//*[@*='${label}']`, `//*[contains(@*, '${label}')]`,
Expand All @@ -677,7 +686,7 @@ Then('So I can see the text {string} in the textbox: {string}', async function c
let resp = await elem.getText();
resp = resp == '' ? await elem.getAttribute('value') : resp;
resp = resp == '' ? await elem.getAttribute('outerHTML') : resp;
match(resp, RegExp(expectedText.toString()), `Textfield does not contain the string/regex: ${expectedText} , actual: ${resp}`);
match(resp, RegExp(resultString), `Textfield does not contain the string/regex: ${resultString} , actual: ${resp}`);
})
.catch(async (e) => {
await driver.takeScreenshot().then(async (buffer) => {
Expand All @@ -689,7 +698,8 @@ Then('So I can see the text {string} in the textbox: {string}', async function c
});

// Search if a is text in html code
Then('So I can see the text: {string}', async function (expectedText) { // text is present
Then('So I can see the text: {string}', async function textPresent(expectedText) { // text is present
const resultString = resolveRegex(expectedText);
const world = this;
try {
await driver.wait(async () => driver.executeScript('return document.readyState').then(async (readyState) => readyState === 'complete'));
Expand All @@ -699,7 +709,7 @@ Then('So I can see the text: {string}', async function (expectedText) { // text
const innerHtmlBody = await driver.executeScript('return document.documentElement.innerHTML');
const outerHtmlBody = await driver.executeScript('return document.documentElement.outerHTML');
const bodyAll = cssBody + innerHtmlBody + outerHtmlBody;
match(bodyAll, RegExp(expectedText.toString()), `Page HTML does not contain the string/regex: ${expectedText}`);
match(bodyAll, RegExp(resultString), `Page HTML does not contain the string/regex: ${resultString}`);
});
} catch (e) {
await driver.takeScreenshot().then(async (buffer) => {
Expand All @@ -711,7 +721,7 @@ Then('So I can see the text: {string}', async function (expectedText) { // text
});

// Search a textfield in the html code and assert if it's empty
Then('So I can\'t see text in the textbox: {string}', async function (label) {
Then('So I can\'t see text in the textbox: {string}', async function textAbsent(label) {
const world = this;
const identifiers = [`//*[@id='${label}']`, `//*[@*='${label}']`, `//*[contains(@id, '${label}')]`, `${label}`];
const promises = [];
Expand Down Expand Up @@ -800,6 +810,7 @@ Then('So the picture {string} has the name {string}', async function checkPictur

// Search if a text isn't in html code
Then('So I can\'t see the text: {string}', async function checkIfTextIsMissing(expectedText) {
const resultString = resolveRegex(expectedText.toString());
const world = this;
try {
await driver.wait(async () => driver.executeScript('return document.readyState').then(async (readyState) => readyState === 'complete'));
Expand All @@ -808,7 +819,7 @@ Then('So I can\'t see the text: {string}', async function checkIfTextIsMissing(e
const innerHtmlBody = await driver.executeScript('return document.documentElement.innerHTML');
const outerHtmlBody = await driver.executeScript('return document.documentElement.outerHTML');
const bodyAll = cssBody + innerHtmlBody + outerHtmlBody;
doesNotMatch(bodyAll, RegExp(expectedText.toString()), `Page HTML does contain the string/regex: ${expectedText}`);
doesNotMatch(bodyAll, RegExp(resultString), `Page HTML does contain the string/regex: ${resultString}`);
});
} catch (e) {
await driver.takeScreenshot().then(async (buffer) => {
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/app/Services/highlight-input.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { HighlightInputService } from './highlight-input.service';

describe('HighlightInputService', () => {
let service: HighlightInputService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(HighlightInputService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
154 changes: 154 additions & 0 deletions frontend/src/app/Services/highlight-input.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Injectable } from '@angular/core';
import { ToastrService } from 'ngx-toastr';

@Injectable({
providedIn: 'root'
})
export class HighlightInputService {

constructor(public toastr: ToastrService) { }
targetOffset: number = 0;

/**
* Add value and highlight regex, Style regex in value and give value to addToValue() function
* Value is in textContent and style is in innerHTML
* If initialCall only check if a regex is already there and hightlight it
* If valueIndex only hightlights regex in first field of regexSteps, only then steps for now. Gets checked with stepPre
* Get cursor position with getCaretCharacterOffsetWithin Helper and set position again, else it is lost because of overwriting and changing the HTML Element in case of hightlighting
* @param element HTML element of contentedible div
* @param initialCall if call is from ngAfterView
* @param isDark theming Service
* @param regexInStory if first regex in Story
* @param valueIndex index of input field
* @param stepPre pre text of step
* @returns if a regex was detected
*/
highlightRegex(element, initialCall?:boolean, isDark?:boolean, regexInStory?:boolean, valueIndex?: number, stepPre?: string) {
const regexPattern =/(\{Regex:)(.*?)(\})(?=\s|$)/g;// Regex pattern to recognize and highlight regex expressions -> start with {Regex: and end with }

const textField = element
const textContent = textField.textContent;
//Get current cursor position
const offset = this.getCaretCharacterOffsetWithin(textField)
const regexSteps = ['So I can see the text', 'So I can see the text:', 'So I can\'t see the text:', 'So I can\'t see text in the textbox:']
var regexDetected = false;

let highlightedText;
if(!valueIndex || (0==valueIndex && regexSteps.includes(stepPre))){
if(isDark){
highlightedText = textContent.replace(regexPattern, (match, match1, match2, match3) => {
regexDetected = true;
return `<span uk-tooltip="title:Regular Expression detected!;pos:right">`+
`<span style="color: var(--light-grey); font-weight: bold">${match1}</span>`+
`<span style="color: var(--light-blue); font-weight: bold">${match2}</span>`+
`<span style="color: var(--light-grey); font-weight: bold">${match3}</span></span>`;
});
} else{
highlightedText = textContent.replace(regexPattern, (match, match1, match2, match3) => {
regexDetected = true;
return `<span uk-tooltip="title:Regular Expression detected!;pos:right">`+
`<span style="color: var(--brown-grey); font-weight: bold">${match1}</span>`+
`<span style="color: var(--ocean-blue); font-weight: bold">${match2}</span>`+
`<span style="color: var(--brown-grey); font-weight: bold">${match3}</span></span>`;
});
}
}
textField.innerHTML = highlightedText ? highlightedText : textContent;

// Toastr logic
if(initialCall && regexDetected) {
regexInStory = true
}
if(regexDetected && !regexInStory){
this.toastr.info('View our Documentation for more Info','Regular Expression detected!');
}

// Set cursor to correct position
if(!initialCall){
if (regexDetected) { //maybe not needed
const selection = window.getSelection();
selection.removeAllRanges()

// Call the function to find the correct node and offset
this.targetOffset = offset
const result = this.findNodeAndOffset(textField);

if (result !== null) {
const [node, offsetIndex] = result;
requestAnimationFrame(() => {
if (node.nodeType === 3) {
// Text node
selection.setBaseAndExtent(node, offsetIndex, node, offsetIndex);
} else if (node.nodeType === 1 && node.childNodes.length > 0) {
// Element node with child nodes (e.g., <span>)
selection.setBaseAndExtent(node.childNodes[0], offsetIndex, node.childNodes[0], offsetIndex);
}
});
}
} else {
requestAnimationFrame(() => {
const selection = window.getSelection();
selection.removeAllRanges();
selection.setBaseAndExtent(textField.firstChild, offset, textField.firstChild, offset)
})
}
}
return regexDetected;
}

/**
* Helper for Regex Highlighter, find right node and index for current cursor position
* @param element HTMLElement
* @returns node: node with cursor, number: offest of cursor in node
*/
findNodeAndOffset(element: Node): [Node, number] | null {
if (element.nodeType === 3) {
// Text node
const textLength = (element.nodeValue || "").length;
if (this.targetOffset <= textLength) {
return [element, this.targetOffset];
} else {
this.targetOffset -= textLength;
}
} else if (element.nodeType === 1){
// Element node
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
const result = this.findNodeAndOffset(child);
if (result !== null) {
return result;
}
}
}
return null;
}

/**
* Helper for Regex Highlighter, extract current cursor position
* @param element HTMLElement
* @returns num, offset of cursor position
*/
getCaretCharacterOffsetWithin(element) {
var caretOffset = 0;
var doc = element.ownerDocument || element.document;
var win = doc.defaultView || doc.parentWindow;
var sel;
if (typeof win.getSelection != "undefined") {
sel = win.getSelection();
if (sel.rangeCount > 0) {
var range = win.getSelection().getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
}
} else if ( (sel = doc.selection) && sel.type != "Control") {
var textRange = sel.createRange();
var preCaretTextRange = doc.body.createTextRange();
preCaretTextRange.moveToElementText(element);
preCaretTextRange.setEndPoint("EndToEnd", textRange);
caretOffset = preCaretTextRange.text.length;
}
return caretOffset;
}
}
24 changes: 24 additions & 0 deletions frontend/src/app/base-editor/base-editor.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -452,3 +452,27 @@ input.background {
padding-top: 0;
}

.contentEditableElement { /*needed for trailing whitespaces*/
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}

.inputEditableDiv{
margin: 1px;
padding: 1px;
border: none;
border-bottom: 1px solid #333;
position: relative;
top: -2px;
width: auto;
min-width: 5px;

margin-left: 10px;
margin-right: 10px;
padding-left: 5px;
padding-right: 5px;
}

Loading

0 comments on commit 019fb9a

Please sign in to comment.