codecpetjs: Helper or Function that can find CSS attribute of a pseudo element (::after) - pseudo-element

I have a codeceptjs/playwright test checking the +/- (opened / closed) indicator on an accordion. The +/- is a background-image applied to the button::after pseudo element.
I have it working as an I.executeScript() function as I have access to window, document, and full DOM elements.
I need to reuse it in a lot of locations and tests so I've been fighting trying to get it working as a helper or even a test specific function.
I can get the helper/function to fire and pass the props in. I just can't get to the ::after pseudo element to get the applied style
I.executeScript(() => {
let cssProp = window
.getComputedStyle(
document.querySelector(
"ul.accordion div#state1 h3.panel-title button"
),
"::after"
)
.getPropertyValue("background-image");
if (cssProp.indexOf("viewBox='0 0 12 12'") > -1) { //plus or minus svg shape
return true;
} else {
return false;
}
});
works flawlessly, but need to be repeated dozens of times, and I'd rather keep it a bit DRY.
helper function checkAccordionPlusMinus works with button element:
returns:
cssProp:: [ 'rgb(244, 244, 244) none repeat scroll 0% 0% / auto
padding-box border-box' ]
let selector = `ul.accordion div#${TestSelector} h3.panel-title button`;
let cssProp = await this.helpers["Playwright"].grabCssPropertyFrom(
selector,
"background"
);
console.log("cssProp:: ", cssProp);
helper function checkAccordionPlusMinus doesn't work with button:after or button::after element:
returns:
cssProp:: undefined
let selector = `ul.accordion div#${TestSelector} h3.panel-title button:after`;
let cssProp = await this.helpers["Playwright"].grabCssPropertyFrom(
selector,
"background-image"
);
console.log("cssProp:: ", cssProp);

Related

Can I set an ag-grid full-width row to have autoHeight?

I am trying to render a set of footnotes at the end of my data set. Each footnote should be a full-width row. On the docs page for row height, it says that you can set an autoHeight property for the column you want to use to set the height. Full-width rows, however, aren't tied to any column, so I don't think there's a place to set that autoHeight property.
For reference, here is my cell renderer, which gets invoked if a flag in the data object is true.
import { Component } from '#angular/core';
import { ICellRendererComp, ICellRendererParams } from '#ag-grid-community/core';
#Component({
template: '',
})
export class FootnoteRendererComponent implements ICellRendererComp {
cellContent: HTMLElement;
init?(params: ICellRendererParams): void {
this.cellContent = document.createElement('div');
this.cellContent.innerHTML = params.data.title;
this.cellContent.setAttribute('class', 'footnote');
}
getGui(): HTMLElement {
return this.cellContent;
}
refresh(): boolean {
return false;
}
}
The footnote (the "title" property above) could be one line or several depending on its length and the browser's window size. There may also be several footnotes. Is there a way to set autoHeight for each footnote row? Thanks for any help!
Not sure of CSS autoHeight can be use, but here is some example for calculating height dynamically. Take a look to getRowHeight function, it's works for any rows (full-width too):
public getRowHeight: (
params: RowHeightParams
) => number | undefined | null = function (params) {
if (params.node && params.node.detail) {
var offset = 80;
var allDetailRowHeight =
params.data.callRecords.length *
params.api.getSizesForCurrentTheme().rowHeight;
var gridSizes = params.api.getSizesForCurrentTheme();
return (
allDetailRowHeight +
((gridSizes && gridSizes.headerHeight) || 0) +
offset
);
}
};
Here is the solution I ended up with, though I like #LennyLip's answer as well. It uses some ideas from Text Wrapping in ag-Grid Column Headers & Cells.
There were two parts to the problem - 1) calculating the height, and 2) knowing when to calculate the height.
1) Calculating the Height
I updated the footnote's Cell Renderer to add an ID to each footnote text node, and used it in the function below.
const footnoteRowHeightSetter = function(params): void {
const footnoteCells = document.querySelectorAll('.footnote .footnote-text');
const footnoteRowNodes = [];
params.api.forEachNode(row => {
if (row.data.dataType === 'footnote') { // Test to see if it's a footnote
footnoteRowNodes.push(row);
}
});
if (footnoteCells.length > 0 && footnoteRowNodes.length > 0) {
footnoteRowNodes.forEach(rowNode => {
const cellId = 'footnote_' + rowNode.data.id;
const cell = _.find(footnoteCells, node => node.id === cellId);
const height = cell.clientHeight;
rowNode.setRowHeight(height);
});
params.api.onRowHeightChanged();
}
};
To summarize, the function gets all HTML nodes in the DOM that are footnote text nodes. It then gets all of the table's row nodes that are footnotes. It goes through those row nodes, matching each up with its DOM text. It uses the clientHeight property of the text node and sets the row node height to that value. Finally, it calls the api.onRowHeightChanged() function to let the table know it should reposition and draw the rows.
Knowing when to calculate the height
When I set the gridOptions.getRowHeight property to the function above, it didn't work. When the function fires, the footnote rows hadn't yet been rendered, so it was unable to get the clientHeight for the text nodes since they didn't exist.
Instead, I triggered the function using these event handlers in gridOptions.
onFirstDataRendered: footnoteRowHeightSetter,
onBodyScrollEnd: footnoteRowHeightSetter,
onGridSizeChanged: footnoteRowHeightSetter,
onFirstDataRendered covers the case where footnotes are on screen when the grid first renders (short table).
onBodyScrollEnd covers the case where footnotes aren't on screen at first but the user scrolls to see them.
onGridSizeChanged covers the case of grid resizing that alters the wrapping and height of the footnote text.
This is what worked for me. I like #LennyLip's answer and looking more into it before I select an answer.

Get all bookmark IDs from Word Document

I want to scan an entire document containing text with the styles Heading 1, Heading 2, normal text and several bullet points / other text (which is basically a tech report). After scanning I want to extract bookmarks assigned to "Heading 2" elements, which also act as sub heading titles within the report.
getBookmarks() is defined in the Preview / Beta API which works if my cursor is placed on the "Heading 2" element, as seen below:
async function getBookmarks() {
Word.run(function(context) {
var range = context.document.getSelection();
var bkmrk = range.getBookmarks(true, true);
return context.sync().then(function() {
console.log("The bookmarks read from the document was: " + bkmrk.value);
});
}).catch(function(error) {
console.log("Error: " + JSON.stringify(error));
if (error instanceof OfficeExtension.Error) {
console.log("Debug info: " + JSON.stringify(error.debugInfo));
}
});
}
I've managed to scan the entire document and obtain the "style" attribute as well, as seen from the example code on API documentation:
async function getParagraphAll() { await Word.run(async (context) => {
// Gets the complete sentence (as range) associated with the insertion point.
let paragraphs = context.document.body.paragraphs
paragraphs.load("text, style");
await context.sync();
// Expands the range to the end of the paragraph to get all the complete sentences.
let completeParagraph = paragraphs.items[0]
.getRange()
.expandTo(
context.document
.getSelection()
.paragraphs.getFirst()
.getRange("End")
)
paragraphs.load("text, style, hyperlink");
await context.sync();
for (let i = 0; i < paragraphs.items.length; i++) {
console.log(paragraphs.items[i].style);
//let range = paragraphs.items[i].getRange() - Why is this not working ?
//let bkmrk = range.getBookmarks(true, false) - This doesnt get me the bookmark while its in
//the loop scanning the entire document. Is it because it fails on "Normal" style?
// Should I filter out "Normal" and only run "getBookmarks" on "Heading" style ?
console.log(paragraphs.items[i].style);
} }); }
I've made the reference to Libraries available in the preview API link: https://appsforoffice.microsoft.com/lib/beta/hosted/office.js
I'm struggling to understand why I can get the bookmark at cursor level but when I want to get it for the entire document, it just displays
do context.sync() before loading any property. There is no need for
load.

How to define cycles with observables

I'm trying to set up the update loop of a simple game, built with observables in mind. The top-level components are a model, which takes input commands, and produces updates; and a view, which displays the received updates, and produces input. In isolation, both work fine, the problematic part is putting the two together, since both depend on the other.
With the components being simplified to the following:
var view = function (updates) {
return Rx.Observable.fromArray([1,2,3]);
};
var model = function (inputs) {
return inputs.map(function (i) { return i * 10; });
};
The way I've hooked things together is this:
var inputBuffer = new Rx.Subject();
var updates = model(inputBuffer);
var inputs = view(updates);
updates.subscribe(
function (i) { console.log(i); },
function (e) { console.log("Error: " + e); },
function () { console.log("Completed"); }
);
inputs.subscribe(inputBuffer);
That is, I add a subject as a placeholder for the input stream, and attach the model to that. Then, after the view is constructed, I pass on the actual inputs to the placeholder subject, thus closing the loop.
I can't help but feel this is not the proper way to do things, however. Using a subject for this seems to be overkill. Is there a way to do the same thing with publish() or defer() or something along those lines?
UPDATE: Here's a less abstract example to illustrate what I'm having problems with. Below you see the code for a simple "game", where the player needs to click on a target to hit it. The target can either appear on the left or on the right, and whenever it is hit, it switches to the other side. Seems simple enough, but I still have the feeling I'm missing something...
//-- Helper methods and whatnot
// Variables to easily represent the two states of the target
var left = 'left';
var right = 'right';
// Transition from one side to the other
var flip = function (side) {
if (side === left) {
return right;
} else {
return left;
}
};
// Creates a predicate used for hit testing in the view
var nearby = function (target, radius) {
return function (position) {
var min = target - radius;
var max = target + radius;
return position >= min && position <= max;
};
};
// Same as Observable.prototype.scan, but it also yields the initial value immediately.
var initScan = function (values, init, updater) {
var initValue = Rx.Observable.return(init);
var restValues = values.scan(init, updater);
return initValue.concat(restValues);
};
//-- Part 1: From input to state --
var process = function (inputs) {
// Determine new state based on current state and input
var update = function(current, input) {
// Input value ignored here because there's only one possible state transition
return flip(current);
};
return initScan(inputs, left, update);
};
//-- Part 2: From display to inputs --
var display = function (states) {
// Simulate clicks from the user at various positions (only one dimension, for simplicity)
var clicks = Rx.Observable.interval(800)
.map(function (v) {return (v * 5) % 30; })
.do(function (v) { console.log("Shooting at: " + v)})
.publish();
clicks.connect();
// Display position of target depending on the model
var targetPos = states.map(function (state) {
return state === left ? 5 : 25;
});
// Determine which clicks are hits based on displayed position
return targetPos.flatMapLatest(function (target) {
return clicks
.filter(nearby(target, 10))
.map(function (pos) { return "HIT! (# "+ pos +")"; })
.do(console.log);
});
};
//-- Part 3: Putting the loop together
/**
* Creates the following feedback loop:
* - Commands are passed to the process function to generate updates.
* - Updates are passed to the display function to generates further commands.
* - (this closes the loop)
*/
var feedback = function (process, display) {
var inputBuffer = new Rx.Subject(),
updates = process(inputBuffer),
inputs = display(updates);
inputs.subscribe(inputBuffer);
};
feedback(process, display);
I think I understand what you are trying to achieve here:
How can I get a sequence of input events going in one direction that feed into a model
But have a sequence of output events going in the other direction that feed from the model to the view
I believe the answer here is that you probably want to flip your design. Assuming an MVVM style design, instead of having the Model know about the input sequence, it becomes agnostic. This means that you now have a model that has a InputRecieved/OnInput/ExecuteCommand method that the View will call with the input values. This should now be a lot easier for you to deal with a "Commands in one direction" and "Events in the other direction" pattern. A sort of tip-of-the-hat to CQRS here.
We use that style extensively on Views+Models in WPF/Silverlight/JS for the last 4 years.
Maybe something like this;
var model = function()
{
var self = this;
self.output = //Create observable sequence here
self.filter = function(input) {
//peform some command with input here
};
}
var viewModel = function (model) {
var self = this;
self.filterText = ko.observable('');
self.items = ko.observableArray();
self.filterText.subscribe(function(newFilterText) {
model.filter(newFilterText);
});
model.output.subscribe(item=>items.push(item));
};
update
Thanks for posting a full sample. It looks good. I like your new initScan operator, seems an obvious omission from Rx.
I took your code an restructured it the way I probably would have written it. I hope it help. The main things I did was encapsulted the logic into the model (flip, nearby etc) and have the view take the model as a parameter. Then I did also have to add some members to the model instead of it just being an observable sequence. This did however allow me to remove some extra logic from the view and put it in the model too (Hit logic)
//-- Helper methods and whatnot
// Same as Observable.prototype.scan, but it also yields the initial value immediately.
var initScan = function (values, init, updater) {
var initValue = Rx.Observable.return(init);
var restValues = values.scan(init, updater);
return initValue.concat(restValues);
};
//-- Part 1: From input to state --
var process = function () {
var self = this;
var shots = new Rx.Subject();
// Variables to easily represent the two states of the target
var left = 'left';
var right = 'right';
// Transition from one side to the other
var flip = function (side) {
if (side === left) {
return right;
} else {
return left;
}
};
// Determine new state based on current state and input
var update = function(current, input) {
// Input value ignored here because there's only one possible state transition
return flip(current);
};
// Creates a predicate used for hit testing in the view
var isNearby = function (target, radius) {
return function (position) {
var min = target - radius;
var max = target + radius;
return position >= min && position <= max;
};
};
self.shoot = function(input) {
shots.onNext(input);
};
self.positions = initScan(shots, left, update).map(function (state) {
return state === left ? 5 : 25;
});
self.hits = self.positions.flatMapLatest(function (target) {
return shots.filter(isNearby(target, 10));
});
};
//-- Part 2: From display to inputs --
var display = function (model) {
// Simulate clicks from the user at various positions (only one dimension, for simplicity)
var clicks = Rx.Observable.interval(800)
.map(function (v) {return (v * 5) % 30; })
.do(function (v) { console.log("Shooting at: " + v)})
.publish();
clicks.connect();
model.hits.subscribe(function(pos)=>{console.log("HIT! (# "+ pos +")");});
// Determine which clicks are hits based on displayed position
model.positions(function (target) {
return clicks
.subscribe(pos=>{
console.log("Shooting at " + pos + ")");
model.shoot(pos)
});
});
};
//-- Part 3: Putting the loop together
/**
* Creates the following feedback loop:
* - Commands are passed to the process function to generate updates.
* - Updates are passed to the display function to generates further commands.
* - (this closes the loop)
*/
var feedback = function (process, display) {
var model = process();
var view = display(model);
};
feedback(process, display);
I presume that because you do not "assign" the inputs after the model is created, you are aiming for a non-mutative approach to instantiating your model and view. However, your model and your view seem to depend on one another. To resolve this issue, you can use a third party to facilitate the relationship between the two objects. In this case, you can simply use a function for dependency injection...
var log = console.log.bind(console),
logError = console.log.bind(console, 'Error:'),
logCompleted = console.log.bind(console, 'Completed.'),
model(
function (updates) {
return view(updates);
}
)
.subscribe(
log,
logError,
logCompleted
);
By providing the model a factory to create a view, you give the model the ability to fully instantiate itself by instantiating it's view, but without knowing how the view is instantiated.
As per my comment on the question itself, here's the same sort of code you're writing done with a scheduler in Windows. I would expect a similar interface in RxJS.
var scheduler = new EventLoopScheduler();
var subscription = scheduler.Schedule(
new int[] { 1, 2, 3 },
TimeSpan.FromSeconds(1.0),
(xs, a) => a(
xs
.Do(x => Console.WriteLine(x))
.Select(x => x * 10)
.ToArray(),
TimeSpan.FromSeconds(1.0)));
The output I get, with three new numbers every second, is:
1
2
3
10
20
30
100
200
300
1000
2000
3000
10000
20000
30000

Coffeescript dynamically create/call a function from a list on select box value change

I'm working on adding some image editing tools using the Pixastic library. The idea is that the user can choose an aspect of the image or tool they want from a select box, then the tool will show up below the select box (I'm using select2) and the user can edit via a slider. Here's what I have so far:
# This seeds the select2 options list
imageToolsList = [
{id: 'bl', text: 'Blur'}
{id: 'bc', text: 'Brightness/Contrast'}
{id: 'ca', text: 'Color Adjust (RGB)'}
...
]
#Creates a select box and calls imageTooler function when the value of the box is changed
$(".image_tools_select").each ->
$(#).select2
placeholder: "Select an adjustment tool."
data: imageToolsList
$(#).on("change", (i) ->
imageTooler JSON.stringify(
val: i.val
clipName: $(#).closest('.clip').attr('id')
)
)
# The function called on the value that the select box is changed to
imageTooler = (i) ->
imageData = jQuery.parseJSON(i)
iId = imageData.val
imageClipName = imageData.clipName
newTool = "<div id=#{iId}><label>#{iId}</label><div class='slider#{iId}'></div></div>"
$("##{imageClipName}").find(".imagetoolfields").append newTool
This succeeds in appending the name of the editing tool and the correct slider div beneath the select box when a tool is chosen, but what I'd really like is dynamically create a slider function for that particular tool and image (there are multiple images on a page, each with their own editing toolbelt). Here's a slider function that works for a the 'Blur' tool:
$('.sliderbl').slider
min: 0
max: 5
value: 0.5
step: 0.1
range: "min"
slide: (event, ui) ->
$("#img_snapshot_16").pixastic("blurfast", {amount:ui.value})
Is there a way to expand the imageToolsList so that it looks something like:
imageToolsList = [
{id: 'bl', text: 'Blur', tool: $("##{imageClipName}").pixastic("blurfast", {amount:ui.value}), sliderVals: {min: 0, max: 5, value: 0.5, step: 0.1, range: "min"} }
...
]
and then dynamically create the jQuery slider functions for each tool in imageTooler, as is being done with the div and slider div?
Comments get a little tedious for anything complicated so I'll just go ahead and map it all out. I've made a few assumptions about what is defined where and when but I don't think the assumptions matter that much.
We'll start with a simplified case: just one object similar to what you have in imageToolsList:
{
id: 'bl'
text: 'Blur'
sliderVals: { min: 0, max: 5, value: 0.5, step: 0.1, range: "min" }
tool: (imageClipName) ->
(event, ui) -> $("##{imageClipName}").pixastic("blurfast", {amount:ui.value})
}
I've tweaked the order a little bit and switched tool to a function which returns a function. We don't want the pixastic call to happen while you're defining the object literals in imageToolsList, making tool a function allows us to defer the pixastic execution until later. Since we (presumably) don't know what imageClipName should be when we define imageToolsList, we need another function to allow us to fill that in with, again, calling pixastic until even later; hence the function returning a function trick.
Given one of these, how do we build a slider call? All we need to do is copy sliderVals (to avoid changing imageToolsList) and fill in the slide function:
sliderDef = { id: 'bl', ... }
doTheSliderThing = (imageClipName) ->
slide = sliderDef.tool(imageClipName)
args = $.extend({ }, sliderDef.sliderVals, slide: slide)
$(".slider#{sliderDef.id}").slider(args)
# And make it all go and pixastic-ify `#pancakes`.
doTheSliderThing('pancakes')
tool is a function which returns a callback function so sliderDef.tool(imageClipName) gives us the appropriate
(event, ui) -> $(...).pixastic(...)
callback function.
If we have an id and we want the appropriate entry from imageToolList, then we have to go looking for it:
# If the list is short:
[sliderDef] = (o for o in imageToolList when o.id == id)
The for loop gives you an array back and then the [sliderDef] unwraps that array and leaves the single result in sliderDef. If the imageToolList is longer then you'd want to short-circuit the loop and bail out as soon as you have a result:
# Longer list, bail out as soon as we've found what we're looking for.
for o in imageToolList when o.id == 2
sliderDef = o
break
or better, rework the structure of imageToolList to allow direct access by id:
# Even longer list: allow direct access by `id`.
imageToolList =
bl: { text: 'Blur', sliderVals: { ... }, ... }
...
and then we can do things like this:
doTheSliderThing = (id, imageClipName) ->
sliderDef = imageToolList[id]
slide = sliderDef.tool(imageClipName)
args = $.extend({ }, sliderDef.sliderVals, slide: slide)
$(".slider#{id}").slider(args)
# And make it all go and pixastic-ify `#pancakes` using `'bl'`.
doTheSliderThing('bl', 'pancakes')
Or, if you prefer to be terse:
doTheSliderThing = (id, imageClipName) ->
$(".slider#{id}").slider($.extend({ }
imageToolList[id].sliderVals
slide: imageToolList[id].tool(imageClipName)
))
Update for the comments: If you have this:
sliderDefs =
bl: { text: 'Blur', sliderVals: { ... }, ... }
...
Then you can build the stuff that slider2 wants like this:
opts = ({id: k, text: v.text} for k,v of sliderDefs)

jsTree Search: show subnodes

How can I make jsTree hide non-matching elements, but still display the subnodes of matched elements?
As far as I know there are only two possibilities, that are both not suited for this case. You can either make jsTree hide no element at all by setting
show_only_matches: false
Or you can make it hide all non-matched elements by setting
show_only_matches: true
But this option also hides the subnodes of matched nodes.
Finally, I found the solution, though it doesn't look really nice, but it works.
Just pass a the found elements the the enableSubtree()-function, and it will show the nodes and take care of the correct appearance (i.e. that the dotted lines are shown and hidden correctly).
enableSubtree = function(elem) {
elem.siblings("ul:first").find("li").show();
return correctNode(elem.siblings("ul:first"));
};
correctNode = function(elem) {
var child, children, last, _j, _len1, _results;
last = elem.children("li").eq(-1);
last.addClass("jstree-last");
children = elem.children("li");
console.log(children);
_results = [];
for (_j = 0, _len1 = children.length; _j < _len1; _j++) {
child = children[_j];
_results.push(correctNode($(child).children("ul:first")));
}
return _results;
};
A call of this function could look like this:
enableSubtree($(".jstree-search"))
since all found nodes receive the CSS class .jstree-search.
"search" : {
"show_only_matches" : true,
"show_only_matches_children" : true
}
Did you try to overload "search.jstree" with something like it ? (with show_only_matches==false)
$("#mytreecontainerid").bind("search.jstree", function (e, data) {
// close the whole tree if a search text in set
if (data.rslt.str.length>0) $("#mytreecontainerid").jstree('close_all');
// open the found nodes' parent
for (var i = 0 ; i<data.rslt.nodes.length ; i++) {
data.inst._get_parent(data.rslt.nodes[i]).open_node(this, false);
/* here you can do any additional effect to your node */
}
});
Here is a simple code I did for the filtering.
filterTree = function (elem) {
$('li a', elem).not(".jstree-search").parent().css('display', 'none');
$('a.jstree-search', elem).parentsUntil(elem, 'li').css('display', 'block');
};
and call it with the jstree instance..
filterTree($treeVar);
It might not be an optimal solution, but it works perfectly :)