My app is slightly unusual in that the appearance is actually critically important. It's sort of a photo manipulation app, so I want to be able to write tests to check, for example, that an element actually has a particular background color. I'm able to find elements with react-testing-library but my jest .toHaveStyle() assertions seem to always pass.
The element under test (rest of component omitted)
<span
className="swatch"
style={{ background: `#${colorToString(color)}` }}
aria-label="original color"
/>
The test
describe('BeadMapRow', () => {
let result: RenderResult;
beforeEach(() => {
const bead = {
brand: 'Test',
code: 'T01',
name: 'Test Bead 1',
color: 0x005080ff
};
result = render(<BeadMapRow color={0x084a8bff} bead={bead} />);
});
it('works', () => {
const e = result.getByLabelText('original color');
console.log(e);
expect(e).toHaveStyle({ 'background-color': '0x084a8bff' });
});
});
Result of that console.log (trimmed)
console.log src/components/beadMapRow.test.tsx:21
HTMLSpanElement {
'__reactInternalInstance$a7if3tsd8xg':
FiberNode {
tag: 5,
key: null,
elementType: 'span',
type: 'span',
stateNode: [Circular],
return:
FiberNode {
/* ... */
},
child: null,
sibling:
FiberNode {
/*...*/
},
index: 0,
ref: null,
pendingProps:
{ className: 'swatch',
style: [Object],
'aria-label': 'original color' },
memoizedProps:
{ className: 'swatch',
style: [Object],
'aria-label': 'original color' },
/* ... */
'__reactEventHandlers$a7if3tsd8xg':
{ className: 'swatch',
style: { background: '#084a8bff' },
'aria-label': 'original color' },
[Symbol(SameObject caches)]:
[Object: null prototype] {
style:
CSSStyleDeclaration {
_values: {},
_importants: {},
_length: 0,
_onChange: [Function] },
childNodes: NodeList {} } }
If I change the expected color to something else, it still passes. The only way so far I've found to make it fail is to say .toHaveStyle('background-color') without a value.
.toHaveStyle() works as intended when you pass colour value types different from the one you are passing, as you can see in this Codesandbox. So it must be something with how you are formatting you colour values.
I think that issue is with the name of the property used to compare the color in the expect, in the component you used the propety background not backgroud-color so try
expect(e).toHaveStyle({ 'background': '#0x084a8bff' });
instead of
expect(e).toHaveStyle({ 'background-color': '0x084a8bff' });
Regards.
Related
This seems like the simplest of requests but I can't seem to retrieve a set of rows from a Tabulator object.
Here's the code which instantiates the Tabulator object.........
function TabulatorInvitees(divId, companyName, userEmail) {
try {
var table = new Tabulator(divId, {
columns: [
{
title: "<div style='width:20%; float:left; text-align:left; color:blue; font-size:14px;'>Vendor Invitees</div>",
columns: [
{ title: "Id", field: "Id", visible: false },
{ title: "Added", field: "Added", visible: false },
{ title: "Changed", field: "Changed", visible: false },
{ title: "MarkedForExclusion", field: "MarkedForExclusion", visible: false },
{ title: "Email Address", field: "Email", widthGrow: 1, responsive: 0, hozAlign: "center", editor: "input", visible: true },
{ title: "First Name", field: "FirstName", widthGrow: 0.5, responsive: 1, hozAlign: "center", editor: "input", visible: true },
{ title: "Last Name", field: "LastName", widthGrow: 0.5, responsive: 1, hozAlign: "center", editor: "input", visible: true }
]
},
{
title: tabulatorAddUser(companyName),
field: "ManageRows",
widthGrow: 0.25,
responsive: 2,
hozAlign: "center",
formatter: "tickCross",
headerClick: function (e, row) {
row.getTable().addRow({ Id: 0, Added: true }, false);
},
cellClick: function (e, cell) {
tabulatorFreezeUnfreezeDelete(cell.getRow());
}
},
],
data: [],
height: "100%",
layout: "fitColumns", // required when using 'widthGrow'
placeholder: tabulatorPlaceholder(companyName), //display message to user on empty table
reactiveData: true, //enable reactive data
responsiveLayout: "collapse",
rowContextMenu: tabulatorContextMenu(),
});
table.on("rowTapHold", function (e, row) {
// from Tabulator documentation: "The rowTapHold event is triggered when a user taps on a row on a touch display and holds their finger down for over 1 second."
//e - the tap event object
//row - row component
tabulatorContextMenu();
});
table.on("tableBuilt", function () {
if (companyName.length > 0) {
table.setData(getDataSync({ caseSelector: "VendorMgmt_EmployeeList", companyCode: companyName, userEmail: userEmail }));
}
else {
table.setData([]);
}
});
}
catch (error) {
console.log(error);
}
}
The setData() function makes a call to a database function which returns three rows, similar to the following:
The following JQuery function is called when a radio button is clicked....
$(".vendorStatus").click(function (e) {
const status = e.target.value;
const tbls = Tabulator.findTable("#divVendorEmployees");
const tbl = tbls[0];
const tblRows = tbl.getRows();
console.log("tbls.length", tbls.length);
console.log("tblRows", tblRows);
});
The browser console indicates a table has been found (tbls.length = 1) but the tblRows array is empty:
I see the three rows in my Tabulator but I am not able to recall them programmatically. It seems like a simple problem which should have a simple answer.
I am using the most recent version of Tabulator (v5.4).
Any assistance is greatly appreciated.
After much searching, I finally came to the realization the DOM element associated with the Tabulator instance must be managed when attempting to refresh or replace data. I've implemented a method which allows me to delete and rebuild the DOM element each time I need to save data to my database and subsequently refresh my Tabulator instance.
Here's the code...
function refreshTabulatorObject(obj) {
let parentId = obj.parentId;
let childId = obj.childId;
//Empty and remove the current version of the [Tabulator] object.
const tables = Tabulator.findTable(childId);
if (tables.length > 0) {
var table = Tabulator.findTable(childId)[0];
table.setData([]);
}
//Remove the existing <div> from the DOM.
$(childId).remove();
//Re-create the <div> element for the [Tabulator] object and append it to the DOM.
var parentDiv = document.getElementById(parentId);
parentDiv.innerHTML = "";
var childDiv = document.createElement("div");
childDiv.setAttribute("id", childId);
parentDiv.appendChild(childDiv);
//Re-create the [Tabulator] object.
TabulatorInvitees("#" + childId, obj.companyName);
}
I'm sure those of you with a more intimate knowledge of Tabulator would likely suggest a more elegant method, however, this one appears to work and I've already spent far more time on this issue that I had anticipated. Unfortunately, elegance is not a luxury I can afford at this point.
I hope this solution might be of help to some other struggling soul.
I'm trying to extend the spacing on Tailwind, but I can't make it work. I did my research and I made the changes in the tailwind.config.js, but when I use the class in the HTML, it doesn't exist.
PS: I understand that there is no need to run the build
tailwind.config.js
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
spacing: {
'1/3': '33,333333%',
'2/3': '66,666667%'
}
},
},
variants: {
extend: {},
},
plugins: [],
}
I've just checked your config and it does create all the classes. The problem is that 33,333333% and 66,666667% are not valid CSS values.
Unlike in Spanish, you have to use decimal points, not commas:
theme: {
extend: {
spacing: {
'1/3': '33.333333%',
'2/3': '66.666667%',
},
},
},
33,333333% is an invalid property value:
33.333333% works fine:
Codesandbox link
I much prefer the look of buttons to a numbered list, but the default handling of prompt in waterfall dialog is to automatically change from buttons (default) to a numbered list after a certain length of content.
I'm currently implementing the prompt like this:
return await step.prompt(FOCUS_AREA_PROMPT, {
prompt: 'Got it. Can you confirm the focus area this is for?',
choices: ChoiceFactory.toChoices(FOCUS_AREAS)
});
I've tried adding a style attribute within this prompt, and also tried adding to the addDialogs line this.dialogs.add(new ChoicePrompt(FOCUS_AREA_PROMPT)); but nothing I have tried has modified the behavior of the options.
I've reviewed the ListStyle enum in the MS docs, but any method I've tried to add these in with has not made any difference. Is there any way to force buttons regardless of content length?
You can set up a choice prompt in the following manner to achieve the buttons look you are seeking. For reference, you can read more about forChannel here.
Alter to match your needs.
Hope of help!
[edit]
Updated below to represent the two ways a Choice Prompt can be assembled and how the value is output (via imBack). When using toChoices, the associated button value is returned in activity.text and in stepContext.result.value (as type Object). When using forChannel, the associated button value is returned in activity.text and in stepContext.result (as type String).
As discussed in the comments, the button title length has a character limit however this is channel specific. When testing in Web Chat, the limit is 20 characters. Adjusting the FOCUS_AREAS "AI & Machine Learning" value (21 chars) to "AI/Machine Learning" (19 chars) results in the choices displaying as buttons and not a list.
Option 1: using toChoices
async choiceStep ( stepContext ) {
const stepResult = stepContext.context.activity.text;
const FOCUS_AREAS = [ 'Chatbots', 'RPA', 'Blockchain', 'AR/VR', 'AI/Machine Learning' ]
if ( stepResult ) {
return await stepContext.prompt( CHOICE_PROMPT, {
prompt: 'Got it. Can you confirm the focus area this is for?',
choices: ChoiceFactory.toChoices( FOCUS_AREAS )
} );
}
}
activity:
{ type: 'message',
id: 'A50eelAPrFIHKv9XeCRm24-o|0000021',
timestamp: 2019-09-25T20:34:30.562Z,
serviceUrl: 'https://directline.botframework.com/',
channelId: 'directline',
from: [Object],
conversation: [Object],
recipient: [Object],
textFormat: 'plain',
locale: 'en-US',
text: 'Chatbots',
channelData: [Object] },
info:
{ index: 1,
options: {},
reason: 'endCalled',
result:
{ value: 'Chatbots', index: 0, score: 1, synonym: 'Chatbots' },
values: { instanceId: 'c10ed437-77eb-4502-cd24-e89d4c5e45cf' },
onNext: [AsyncFunction: onNext] }
Option 2: using forChannel
async choiceStep ( stepContext ) {
const stepResult = stepContext.context.activity.text;
if ( stepResult ) {
const message = ChoiceFactory.forChannel(
stepContext.context, [
{ value: 'Chatbots', action: { type: 'imBack', title: 'Chatbots', value: 'Chatbots' } },
{ value: 'RPA', action: { type: 'imBack', title: 'RPA', value: 'RPA' } },
{ value: 'Blockchain', action: { type: 'imBack', title: 'Blockchain', value: 'Blockchain' } },
{ value: 'AR/VR', action: { type: 'imBack', title: 'AR/VR', value: 'AR/VR' } },
{ value: 'AI/Machine Learning', action: { type: 'imBack', title: '', value: 'AI/Machine Learning' }, text: 'AI/Machine Learning' },
], `Which do you choose?`
);
await stepContext.context.sendActivity( message );
}
return { status: DialogTurnStatus.waiting };
}
activity:
{ type: 'message',
id: 'Cw5xvHTv6RCDWf3kkyS3Ir-o|0000205',
timestamp: 2019-09-25T20:21:30.320Z,
serviceUrl: 'https://directline.botframework.com/',
channelId: 'directline',
from: [Object],
conversation: [Object],
recipient: [Object],
textFormat: 'plain',
locale: 'en-US',
text: 'Chatbots',
channelData: [Object] },
info:
{ index: 1,
options: {},
reason: 'continueCalled',
result: 'Chatbots',
values: { instanceId: '4becefed-88d2-773e-6184-91456609a26a' },
onNext: [AsyncFunction: onNext] }
As the topic states, I would like to group countries to create my own "areas". It works nearly, but I don't know whats wrong.
Here is my map: http://jsfiddle.net/wiesson/oktajn6e
It is mostly derived from the examples, but it does not work. If I set "allAreas" to false, it is okay but I would like to display all other countries, too!
Any ideas?
$(function () {
// Instanciate the map
$('#container').highcharts('Map', {
chart: {
borderWidth: 0
},
title: {
text: 'Group Hover'
},
legend: {
enabled: true
},
plotOptions: {
map: {
allAreas: true,
joinBy: ['iso-a2', 'code'],
mapData: Highcharts.maps['custom/world']
},
series: {
states:{
normal: {
animation: false
}
},
point: {
events: {
mouseOver: function(){
var ser = this.series;
var data = ser.data;
$.each(data, function(){
this.setState("hover")
});
},
mouseOut: function(){
var ser = this.series;
var data = ser.data;
$.each(data, function(){
this.setState()
});
}
}
}
}
},
series: [{
name: 'Nordic Countries',
data: $.map(['IS', 'NO', 'SE', 'FI', 'DK'], function (code) {
return {
code: code
};
}),
}, {
name: 'Some of central Europe',
data: $.map(['DE', 'AT', 'GB', 'FR'], function (code) {
return {
code: code
};
}),
}]
});
});
This solution should fix your problem: http://jsfiddle.net/oktajn6e/5/
But let me explain what happens in your code:
With both series you create a full world map with all areas. So if you activate both series, the second series covers the complete first series.
It means, the blue areas get covered by series' two grey areas.
I managed to solve it this way:
series: [{
allAreas: true,
mapData: Highcharts.maps['custom/world'],
showInLegend: false,
}, {
allAreas: false,
name: 'Nordic Countries',
joinBy: ['iso-a2', 'code'],
mapData: Highcharts.maps['custom/world'],
data: $.map(['IS', 'NO', 'SE', 'FI', 'DK'], function (code) {
return {
code: code
};
}),
}, {
allAreas: false,
name: 'Some of central Europe',
joinBy: ['iso-a2', 'code'],
mapData: Highcharts.maps['custom/world'],
data: $.map(['DE', 'AT', 'GB', 'FR'], function (code) {
return {
code: code
};
}),
}]
By creating each series individually and setting "allAreas:false" we can simply render it on the first series, where we only show the full map.
I have a multiselect with search bound to a store (with attribute string_value). Search only searches strings that start with "string to search" instead of contains "string to search" (similar to searching for '%string%' instead of 'string%'). Is there a way to do this by extending 'multiselector-search'?
Below is my multiselector control bound to a form:
var ms = Ext.widget('form', {
xtype: 'multi-selector',
width: 400,
height: 300,
requires: [
'Ext.view.MultiSelector'
],
layout: 'fit',
renderTo: Ext.getBody(),
items: [{
bbar: [{
xtype: 'button',
itemId: 'button',
html: 'Toolbar here',
text: 'Submit request to API',
// get submitted array
handler: function() {
if (cardioCatalogQT.config.mode === 'test') {
console.log('In submitted values handler: ');
}
var submitted = Ext.getCmp('test');
var dx = [];
Ext.Array.each(submitted.store.data.items, function (item) {
dx.push(item.data.string_value);
}); // each()
Ext.Msg.alert('Submitted Values',
'The following diagnoses will be sent to the server: <br
/>' + dx);
if (cardioCatalogQT.config.mode === 'test') {
console.log(dx);
}
}
}],
xtype: 'multiselector',
title: 'Selected Dx',
id: 'test',
name:'test',
fieldName: 'string_value',
viewConfig: {
deferEmptyText: false,
emptyText: 'No Dx selected'
},
// TODO: fix ability to remove selected items when box is unchecked
search: {
field: 'string_value',
store: 'Diagnoses'
}
}]
}).center();
The closest I could find to this problem was http://www.sencha.com/forum/showthread.php?240887. I tried making it work with the multiselect search with no success.
The MultiSelector uses the Ext.util.Filter to narrow the results based on the typed text. You need to enable the anyMatch property for it to match anywhere.
To do that, you'll have to include a new "search" function in your multiselector's search object that will have anyMatch=true.
Please see my fiddle, https://fiddle.sencha.com/#fiddle/jf5, for an example of how to do this.