How to unit test HTML being rendered from JSON? - javascript

I am having a little trouble working out how to UNIT test with JEST and vanilla JS as nothign is really coming up online.
I am calling an API endpoint and then rendering that data as HTML - A set of UL/ LI's and a sub menu if there is a submenu.
How would you go about breaking this function up to unit test it - I am not even really sure where to start
Here is the data
"items":[
{
"label":"Work",
"url":"#/work",
"items":[
]
},
{
"label":"About",
"url":"#/about",
"items":[
{
"label":"What we do",
"url":"#/about/what-we-do"
},
{
"label":"How we work",
"url":"#/about/how-we-work"
},
{
"label":"Leadership",
"url":"#/about/leadership"
}
]
},
{
"label":"foo",
"url":"#/foo",
"items":[
{
"label":"Client Services",
"url":"#/foo/client"
},
{
"label":"Creative",
"url":"#/foo/creative"
},
{
"label":"Motion & Media",
"url":"#/foo/motion"
}
]
}
]
}
Here is the function I am calling to create the DOM elements - Its slightly messy but it is essentially creating the anchor tags and Ul / Li's.
createNavigationMenu: function (data) {
return data.map((item) => {
const listElement = document.createElement('li');
const listElementAnchor = document.createElement('a');
const subMenuContainer = document.createElement('ul');
const chevron = document.createElement('span');
listElementAnchor.setAttribute("href", `${item.url}`);
listElementAnchor.setAttribute("class", 'navigation__primary-menu-anchor');
listElementAnchor.innerHTML = item.label;
listElement.setAttribute("class", "navigation__primary-menu-item");
listElement.appendChild(listElementAnchor);
this.navigationContainer.append(listElement);
subMenuContainer.setAttribute("class", "navigation__submenu");
item.items.length ? listElement.append(subMenuContainer) : null
chevron.setAttribute("class", "arrow");
item.items.length ? listElementAnchor.append(chevron) : null
return item.items.map(submenuItem => {
const subMenuContainerItem = document.createElement('li');
const subMenuContainerItemAnchor = document.createElement('a');
subMenuContainerItemAnchor.setAttribute("href", `/${submenuItem.url}`);
subMenuContainerItemAnchor.setAttribute("class", 'navigation__submenu-menu-anchor');
subMenuContainerItemAnchor.innerHTML = submenuItem.label;
subMenuContainerItem.setAttribute("class", "navigation__submenu-menu-item");
subMenuContainerItem.append(subMenuContainerItemAnchor)
listElement.append(subMenuContainer);
subMenuContainer.append(subMenuContainerItem)
})
})
}
I have tried this with JSDOM but it does not seem to work
const data = {
"items": [
{
"label": "Work",
"url": "#/work",
"items": [
]
}
]
}
const markup = `<ul id="navigation__primary-menu" class="navigation__primary-menu">
<li>
<h1 class="navigation__primary-logo">HUGE </h1> <span id="iconSpan" class="saved"> </span>
</li>
<li class="navigation__primary-list-item"></li>
<li class="navigation__primary-menu-item">Work</li>
</ul>`
describe('should map data correctly', () => {
test('markup entered', () => {
const { windpw } = new JSDOM(markup);
const nav = Controller.createNavigationMenu(data)
expect(dom.serialize()).toMatch(nav)
});
});

This answer is for guidance only. It will not 'just work.'
Your request is complex - tests are usually written in Node (which is server side) and you are talking about using document and the DOM (which is client side.)
I would suggest looking into https://github.com/jsdom/jsdom which allows you to emulate document in Node.
Then, from their docs, something like:
const dom = new JSDOM();
and update your generator like this:
createNavigationMenu: function (data, dom) { //pass in your dom object here
return data.map((item) => {
const listElement = dom.createElement('li');
.... //replace all documents with dom
Pass your special dom into your generator function when running tests, and pass the true document at other times.
Finally, in your test (using jest example):
describe('dom test', () => {
it('should render like this', () => {
expect(dom.serialize()).toMatchSnapshot()
}
}
As an aside, DOM manipulation is usually only necessary in this day and age if you're trying to be super clever. I strongly suggest (if you haven't considered it) trying to use a framework that is going to make your life much easier. I use React, which also comes with the added bonus of testing frameworks that allow you to test rendering. Other frameworks are available.

Related

Jest testing async code with then and catch present inside the function

I have to add unit testing to legacy code where I can make no changes inside the code hence can't refactor the code to add async/await or increase the jest version from 25.0.0
code file (sample code of actual implementation):
window.getUsers = function(){
$.get(url)
.then(data => {
data.forEach(val => {
$(".box-user .box-content").append("<h1>hello</h1>");
console.log('box users content code file',document.querySelector('.box-users > .box-content').innerHTML);
})
})
.catch(err => {})
}
test case i am trying to run
import path from 'path';
import '#testing-library/jest-dom/extend-expect';
const fs = require('fs');
const html = fs.readFileSync(path.resolve(__dirname, './code-file.html'),'utf8');
const $ = require('jquery');
const mockResponse = {
"data": [
{
"id": "test id",
"name": "test name",
"surname": "test surname",
"jobTitle": "test job title",
}
],
"length": 1
};
describe("code-file",()=>{
document.documentElement.innerHTML = html;
it("api mocking and appending html ",()=>{
$.get = jest.fn().mockImplementation(() => {
return Promise.resolve(mockResponse);
});
let {getUsers} = require("./code-file");
getUsers();
let boxUsersHtml = document.querySelector(".box-users > .box-content").innerHTML;
console.log('box users content testing file',boxUsersHtml)
})
})
as you can see inner html for testing code is empty while I do see inner html of code file to have some value. I believe this is due to the async behavior where in the test case is complete and the html has not been appended yet because it is present inside a then block.
need to know how to execute then block before any assertions(before console in the above code) are made without making changes in actual code.
done function did not help link for reference testing async code

How can I add an Algolia index to Gatsby GraphQL?

I would like to obtain all (or a subset) of my records from an Algolia index and access them via GraphQL.
I know there is a Gatsby plugin that allows you to do the opposite i.e., add data from a GraphQL query to Algolia, but not the other way around.
I have been able to get the tutorial for adding GraphQL data to work, but I have not had any success when trying to go beyond hardcoded arrays (this is in the gatsby-node.js file):
const algoliasearch = require("algoliasearch/lite");
const searchClient = algoliasearch(
process.env.GATSBY_ALGOLIA_APP_ID,
process.env.GATSBY_ALGOLIA_SEARCH_KEY
)
const searchIndex = searchClient.initIndex(process.env.GATSBY_ALGOLIA_INDEX_NAME)
exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
searchIndex.search("", {
attributesToRetrieve: ["name", "url"]
}).then(({ hits }) => {
hits.forEach(hit => {
const node = {
name: hit.name,
url: hit.url,
id: createNodeId(`hit-${hit.name}`),
internal: {
type: "hit",
contentDigest: createContentDigest(hit),
},
}
actions.createNode(hit)
})
});
}
While the console successfully logs the array of nodes, and the verbose Gatsby deploy output includes the "hit" node as a node type, they do not appear in the GraphQL explorer.
Any help is greatly appreciated, thank you!

Firebase Realtime Database - Set up multiple .on() listeners based on access nodes

I am utilising a Firebase Realtime Database with the below structure.
I wish to fetch all "notes" a user has access to and subscribe to changes in those notes.
notes: {
"noteId-1345" : {
"access" : {
"author": "1234567890"
"members": {
"1234567890": 0 <--- Author
"0987654321": 1 <--- Member
}
},
"data" : {
"title": "Hello",
"content": "Konichiwa!",
"comment": "123"
}
}
}
( I am aware this structure could, ideally, be more flat. :) )
To fetch all notes a user has access to - I keep an additional user_notes node in the root:
Whenever I associate a user (update of members) with a note, I update both /notes/$noteid and /user_notes/$uid.
user_notes: {
"$uid": {
"noteId-1345": {
myHide: false,
mySortOrder: 0,
title: "Hello"
}
}
}
When fetching data I wish to set up subscription to all notes the user has access to.
I begin by fetching the ids for notes the user has access to and then attach listeners to subscribe to updates in each note.
const uid = getState().auth.uid
let collectedNotes = {}
...
database.ref(`user_notes/${uid}`).on('value', (myAccessSnaps) => {
myAccessSnaps.forEach((accessSnap) => {
const noteId = accessSnap.key
const privateData = {'personalData': {...accessSnap.val()}}
database.ref(`notes/${noteId}`).on('value', (noteSnap)=>{
const notData = noteSnap.val()
const fullData = { ...privateData, ...notData }
const note = {
id: noteSnap.key,
...fullData
}
collectedNotes[note.id] = note
...
})
}))
})
(Of course, I will need to use .off() to detach listeners before setting up new ones)
This is somewhat problematic since I have to attach one listener per note - and there could be hundreds of notes in the database.
Is this the most efficient approach? - It seems inefficient.
Is there a way to listen to ALL the notes a user has acess to in the /notes path with one listener? Or is my approaching altogether wrong? :)
Kind regards /K
After understanding that .on() does not return a promise - I became much easier to solve my problem.
Attaching a lot of .on() listeners did not make any sense to me.
The easiest approach for me was to:
1 - Update my access nodes with a time stamp updatedAt each time a note was updated
2 - Load initial data using .once() that returns promise,see code below.
3 - Set up separate subscription for when access nodes changes
let myPromises = []
database.ref(`user_notes/${uid}`).once('value', (myAccessSnaps) => {
myAccessSnaps.forEach((accessSnap) => {
const noteId = accessSnap.key
const privateData = {'personalData': {...accessSnap.val()}}
myPromises.push(
database.ref(`notes/${noteId}`).once('value', (noteSnap)=>{
const notData = noteSnap.val()
const fullData = { ...privateData, ...notData }
const note = {
id: noteSnap.key,
...fullData
}
collectedNotes[note.id] = note
...
})
)
}))
})
return Promise.all(myPromises)
.then(() => {
dispatch(setNotes(categories))
...
// Set up subscription after initial load
database.ref(`user_notes/${uid}`).on('value', (myAccessSnaps) => {
...
// Use access node listener only - gets updated by 'updatedAt'
database.ref(`notes/${noteId}`).once('value', (noteSnap)=>{
//Collect and dispatch data
Kind regards /K

How to customize the onMouseOver gitgraph-js

I am trying to utilize the library #gitgraph/js in my application (Note: I cannot use the React or NodeJS version, only the plain JS):
https://github.com/nicoespeon/gitgraph.js/tree/master/packages/gitgraph-js
Here is an example of what I am trying to do:
https://jsfiddle.net/Ben_Vins/fwcah5s0/7/
var myTemplateConfig = {
// … any specific template configuration
commit: {
shouldDisplayTooltipsInCompactMode: true, // default = true
tooltipHTMLFormatter: function(commit) {
return "<b>BV" + commit.sha1 + "</b>" + ": " + commit.message;
},
}
};
// Instantiate the graph.
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
mode: GitgraphJS.Mode.Compact,
template: new GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, myTemplateConfig ),
});
// Simulate git commands with Gitgraph API.
const master = gitgraph.branch("master");
master.commit("Initial commit");
const develop = gitgraph.branch("develop");
develop.commit("Add TypeScript");
const aFeature = gitgraph.branch("a-feature");
aFeature
.commit("Make it work")
.commit({ subject: "Make it right", hash: "test" })
.commit("Make it fast");
develop.merge(aFeature);
develop.commit("Prepare v1");
master.merge(develop).tag("v1.0.0");
By default, the result were too big, so I have applied a css to scale down the graph (the graph is an inline SVG without a cropbox property, so this is the only trick I could find).
What I would like to do:
Customize the tooltip of the onhover of the commits node (making it larger, change the text, change its css if possible)
Add a onclick event to capture the commit (in particular the commit hash to be used elsewhere in my application)
Extra points:
The documentation is limited and the examples from
https://github.com/nicoespeon/gitgraph.js/tree/master/packages/stories/src/gitgraph-js/
are in typescript. Are they also applicable for the JS version of gitgraph-js?
Note that the documentation of gitgraph.js seemed more detailed i.e. https://github.com/nicoespeon/gitgraph.js/blob/master/packages/gitgraph-js/MIGRATE_FROM_GITGRAPH.JS.md but I was trying to use the next version i.e. #gitgraph/js
Thanks for reaching out. I can add more colors to this as the author of the lib.
To customize the tooltip of a commit, you can provide a renderTooltip() custom function. It will give you the reference to the commit object, so you can customize at your will.
Same, you can pass a onClick() function that will give you the reference to the commit object
The examples are in TypeScript, but that's valid JS if you simply remove the types. Also, the gitgraph-js stories looks like they are React code, but they're not. They're simply wrapped in a React component so we could run them in Storybook along with the gitgraph-react ones.
With the latest version of the lib, you could try the following:
const graphContainer = document.getElementById("graph-container");
// Instantiate the graph.
const withoutBranchLabels = GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, {
branch: { label: { display: false } },
});
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
mode: GitgraphJS.Mode.Compact,
template: withoutBranchLabels,
});
// Simulate git commands with Gitgraph API.
let storedCommit;
gitgraph
.commit({
subject: "Initial commit",
onClick: (commit) => storedCommit = commit
})
.commit({
subject: "Another commit",
onClick: (commit) => storedCommit = commit
})
.commit({
subject: "Do something crazy",
renderTooltip,
onClick: (commit) => storedCommit = commit
});
gitgraph
.branch("dev")
.commit({
subject: "Oh my god",
renderTooltip,
})
.commit({
subject: "This is a saxo!",
renderTooltip,
});
// Logs in console the sha1 of the clicked commit every 3s (for master branch only)
setInterval(() => storedCommit && console.log(`stored commit sha1: ${storedCommit.hashAbbrev}`), 3000)
// Custom tooltip renderer
function renderTooltip (commit) {
const commitSize = commit.style.dot.size * 2;
return createG({
translate: { x: commitSize + 10, y: commitSize / 2 },
children: [
createText({
fill: commit.style.dot.color,
content: `BV${commit.hashAbbrev}: ${commit.subject}`
})
],
});
}
// Helper functions to create SVGs
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
function createText(options) {
const text = document.createElementNS(SVG_NAMESPACE, "text");
text.setAttribute("alignment-baseline", "central");
text.setAttribute("dominant-baseline", "central");
text.textContent = options.content;
if (options.bold) {
text.setAttribute("font-weight", "bold");
}
if (options.fill) {
text.setAttribute("fill", options.fill);
}
if (options.font) {
text.setAttribute("style", `font: ${options.font}`);
}
if (options.anchor) {
text.setAttribute("text-anchor", options.anchor);
}
if (options.translate) {
text.setAttribute("x", options.translate.x.toString());
text.setAttribute("y", options.translate.y.toString());
}
if (options.onClick) {
text.addEventListener("click", options.onClick);
}
return text;
}
function createG(options) {
const g = document.createElementNS(SVG_NAMESPACE, "g");
options.children.forEach((child) => child && g.appendChild(child));
if (options.translate) {
g.setAttribute(
"transform",
`translate(${options.translate.x}, ${options.translate.y})`,
);
}
if (options.fill) {
g.setAttribute("fill", options.fill);
}
if (options.stroke) {
g.setAttribute("stroke", options.stroke);
}
if (options.strokeWidth) {
g.setAttribute("stroke-width", options.strokeWidth.toString());
}
if (options.onClick) {
g.addEventListener("click", options.onClick);
}
if (options.onMouseOver) {
g.addEventListener("mouseover", options.onMouseOver);
}
if (options.onMouseOut) {
g.addEventListener("mouseout", options.onMouseOut);
}
return g;
}
You can't just return HTML though, it must be SVG because the current renderer only handles SVG. That is surely less convenient than before, thus I encourage you to build helper functions like I did here. You can find helpers used in the stories too.
I hope that will be helpful. You can play with the new online playground too: https://codepen.io/nicoespeon/pen/arqPWb?editors=1010
Finally, I'm not maintaining the library much and I'm still looking for active maintainers: https://github.com/nicoespeon/gitgraph.js/issues/328

How to process two sets from different models in one custom control

Aim:
I'd like to have two models(sets of data) passed to the custom control with a predefined search field, in which later on I can execute filtering.
I'm a newbie in OpenUi5, so I might be doing something wrong and stupid here. I've started with a simplified task of passing data from the frontend to my custom control and experiencing troubles.
Background of the simplified idea:
Create a custom control with an aggregation foo , the value to it will be provided from the view.
Also create another aggregation element _searchField which will be populated with the data provided from the view.
Fire the onSuggestTerm everytime user types in a _searchField.
Custom control code:
function (Control) {
var DropDownListInput = Control.extend('xx.control.DropDownListInput', {
metadata: {
defaultAggregation: 'foo',
aggregations: {
foo: { type: 'sap.m.SuggestionItem', multiple: true, singularName: 'suggestionItem' },
_searchField: { type: 'sap.m.SearchField', multiple: false, visibility: 'hidden' }
}
}
});
DropDownListInput.prototype.init = function () {
var that = this;
this.onSuggestTerm = function (event) {
var oSource = event.getSource();
var oBinding = that.getAggregation('foo');
oBinding.filter(new sap.ui.model.Filter({
filters: new sap.ui.model.Filter('DISEASE_TERM', sap.ui.model.FilterOperator.Contains, ' Other')
}));
oBinding.attachEventOnce('dataReceived', function () {
oSource.suggest();
});
};
this.setAggregation('_searchField', new sap.m.SearchField({
id: 'UNIQUEID1',
enableSuggestions: true,
suggestionItems: that.getAggregation('foo'),
suggest: that.onSuggestTerm
}));
};
return DropDownListInput;
}, /* bExport= */true);
I'm not providing Renderer function for control here, but it exists and this is the most important excerpt from it:
oRM.write('<div');
oRM.writeControlData(oControl);
oRM.write('>');
oRM.renderControl(oControl.getAggregation('_searchField'));
oRM.write('</div>');
Passing the data to this control from the xml frontend:
<xx:DropDownListInput
id="diseaseTermUNIQUE"
foo='{path: db2>/RC_DISEASE_TERM/}'>
<foo>
<SuggestionItem text="{db2>DISEASE_TERM}"
key="{db2>DISEASE_TERM}" />
</foo>
</xx:DropDownListInput>
The code fails to run with this error Cannot route to target: [object Object] -
and I have no idea what's wrong here..
The problem is that you forgot to provide single quotes in your path:
foo="{path: 'db2>/RC_DISEASE_TERM/'}"

Categories