I have been using Cypress.io to run end-to-end tests. Recently, I have been using it to run unit tests as well. However, I have some issues with some small helper functions that I have built with NodeJs.
I have a create file called utils.spec.js in the following path <my-project-name>/cypress/integration/unit/utils.spec.js & I have written the following tests:
File: utils.spec.js
// Path to utils.js which holds the regular helper javascript functions
import { getTicketBySummary } from '../../path/to/utils';
describe('Unit Tests for utils.js methods', () => {
/**
* Array of objects mocking Tickets Object response
*/
const mockedTickets = {
data: {
issues: [
{
id: 1,
key: 'ticket-key-1',
fields: {
summary: 'This is ticket number 1',
},
},
{
id: 2,
key: 'ticket-key-2',
fields: {
summary: 'This is ticket number 2',
},
},
],
},
};
const mockedEmptyTicketsArray = [];
it('returns an array containing a found ticket summary', () => {
expect(
getTicketBySummary({
issues: mockedTickets,
summaryTitle: 'This is ticket number 1',
})
).eq(mockedTickets.data.issues[0]);
});
it('returns an empty array, when no ticket summary was found', () => {
expect(
getTicketBySummary({
issues: mockedTickets,
summaryTitle: 'This is ticket number 3',
})
).eq(mockedEmptyTicketsArray);
});
});
File: utils.js
const fs = require('fs');
/**
* Method to search for an issue by its title
* Saving the search result into an array
*
* #param {array} - issues - an array containing all existing issues.
* #param {string} - summaryTitle - used to search an issue by title
*/
const getTicketBySummary = ({ issues, summaryTitle }) =>
issues.data.issues.filter(issueData => {
return issueData.fields.summary === summaryTitle ? issueData : null;
});
/**
* Method to read a file's content
* Returns another string representing the file's content
*
* #param {str} - file - a passed string representing the file's path
*/
const readFileContent = file => {
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf8', (err, data) => {
if (err) return reject(err);
return resolve(data);
});
});
};
module.exports = { getTicketBySummary, readFileContent };
However, when I run the command: npx cypress run --spec=\"cypress/integration/unit/utils.spec.js\" --env mode=terminal, I get the error:Module not found: Error: Can't resolve 'fs'.
Also if I commented out the fs import & its function I get another error:
1) An uncaught error was detected outside of a test:
TypeError: The following error originated from your test code, not from Cypress.
> Cannot assign to read only property 'exports' of object '#<Object>'
When Cypress detects uncaught errors originating from your test code it will automatically fail the current test.
Cypress could not associate this error to any specific test.
We dynamically generated a new test to display this failure
I did some digging on the second error & it seems the describe method is defined. How can I fix both issues? What am I doing wrong?
You can use tasks to execute node code. In your plugins.js create the task with the arguments you need, returning the calculated value:
on('task', {
// you can define and require this elsewhere
getTicketBySummary('getTicketBySummary', { issues, summaryTitle }) {
return issues.data.issues.filter(...);
}
})
}
In your test, execute the task via cy.task:
it('returns an array containing a found ticket summary', () => {
cy.task('getTicketBySummary', {
issues: mockedTickets,
summaryTitle: 'This is ticket number 1',
}).then(result => {
expect(result).eq(mockedTickets.data.issues[0]);
})
});
That being said, getTicketBySummary looks like a pure function that doesn't depend on fs. Perhaps separate helper functions that actually need node as that could avoid need cy.task. If you want to be able to import commonjs (require) via ES6 import/export you would usually need to setup build tools (babel/rollup/etc) to be able to resolve that effectively.
Hopefully that helps!
Related
I have the following template.yaml from a SAM application
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
image-resizing-lambda-js
Sample SAM Template for image-resizing-lambda-js
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
MemorySize: 1536
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs10.x
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: post
imagemagicklambdalayer:
Type: AWS::Serverless::Application
Properties:
Location:
ApplicationId: arn:aws:serverlessrepo:us-east-1:145266761615:applications/image-magick-lambda-layer
SemanticVersion: 1.0.0
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
With the following code.
Now I have read that I need to do the following to use ImageMagick with node on aws lambda is the following
Install the custom layer https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:145266761615:applications~image-magick-lambda-layer
Link any lambda function using ImageMagick with that custom layer
It is the link any lambda function using ImageMagick with the custom layer I am confused. Do I need to do something different in my app.js code that points the imagemagick call in my code to the layer somehow. I am not entirely sure what a layer is. But my understanding is it is needed for ImageMagick to work.
Any help would be greatly appreciated
const axios = require("axios");
// const url = 'http://checkip.amazonaws.com/';
//const sharp = require("sharp");
const gm = require("gm");
const imageMagick = gm.subClass({ imageMagick: true });
let response;
/**
*
* Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
* #param {Object} event - API Gateway Lambda Proxy Input Format
*
* Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
* #param {Object} context
*
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
* #returns {Object} object - API Gateway Lambda Proxy Output Format
*
*/
exports.lambdaHandler = async (event, context) => {
try {
// const ret = await axios(url);
const parsedBody = JSON.parse(event.body);
response = {
statusCode: 200,
body: JSON.stringify({
message: parsedBody.imagePath,
// location: ret.data.trim()
}),
};
const WEB_WIDTH_MAX = 420;
const WEB_Q_MAX = 85;
const url = "https://miro.medium.com/max/512/1*V395S0MUwmZo8dX2aezpMg.png";
const data = imageMagick(url)
.resize(WEB_WIDTH_MAX)
.quality(WEB_Q_MAX)
// .gravity('Center')
.strip()
// .crop(WEB_WIDTH_MAX, WEB_HEIGHT_MAX)
.toBuffer("png", (err, buffer) => {
if (err) {
console.log("An error occurred while saving IM to buffer: ", err);
return false; /* stop the remaining sequence and prevent sending an empty or invalid buffer to AWS */
} else {
console.log("buffer", buffer);
}
});
// gmToBuffer(data).then(console.log);
} catch (err) {
console.log(err);
return err;
}
return response;
};
Currently I get the following error when running sam build
Plugin 'ServerlessAppPlugin' raised an exception: 'NoneType' object has no attribute 'get'
Before addign the imagemagicklamdalayer section I was able to run sam build and have imagemagick run with the following error under sam local start-api after hitting the endpoint
An error occurred while saving IM to buffer: Error: Stream yields empty buffer
I have got it to build with the following as the layer in the template.yaml
Layers:
- "arn:aws:serverlessrepo:us-east-1:145266761615:applications/image-magick-lambda-layer"
Now I get an error on running the function like the following
File "/usr/local/Cellar/aws-sam-cli/1.46.0/libexec/lib/python3.8/site-packages/botocore/regions.py", line 230, in _endpoint_for_partition
raise NoRegionError()
botocore.exceptions.NoRegionError: You must specify a region.
Not sure where I am to specifiy this region
UPDATE:
Ok two things one I set the region in my aws config file and I set the arn to the deployed layer. I have gotten the following with something that hasn't built yet in about 5 minutes so we shall see if it ever does.
It tries to build the layer when the function is invoked
Invoking app.lambdaHandler (nodejs12.x)
arn:aws:lambda:us-east-1:396621406187:layer:image-magick:1 is already cached. Skipping download
Image was not found.
Building image...................................................................
UPDATE:
so it seemed to finally build but the module for ImageMagick could not be found. I have seen other examples use this layer and are calling imagemagick from a module called gm which is what I am doing in my code.
Does including this layer not give access to gm module so i can use imageMagick.
{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'gm'\nRequire stack:\n- /var/task/app.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js","stack":["Runtime.ImportModuleError: Error: Cannot find module 'gm'","Require stack:","- /var/task/app.js","- /var/runtime/UserFunction.js","- /var/runtime/index.js"," at _loadUserApp (/var/runtime/UserFunction.js:100:13)",
I have below method in my controller. How can I test it? Annotation coverage is failing. Actually I am not able to define code for coverage in jest file.
What parameter I should add in my test file and pass to cover it?
#Post('upload')
#UseInterceptors(
FileInterceptor('image', {
storage: diskStorage({
destination: './uploads',
filename(_, file, callback) {
const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');
return callback(null, `${randomName}${extname(file.originalname)}`);
},
}),
})
)
uploadFile(#UploadedFile() file) {
return {
url: `http://localhost:2300/api/inventory/uploads/${file.filename}`,
originalFileName: `${file.originalname}`,
};
}
It is working perfectly. I wrote this jest test case for same.
it('Single file upload', async () => {
const response = await controller.uploadFile(mockFile);
const fileService = new InventoryController(service);
expect(response).toBeTruthy();
expect(response).toStrictEqual(
expect.objectContaining({
originalFileName: expect.any(String),
url: expect.any(String),
}),
);
});
This is successful but coverage is failing for this line.
#UseInterceptors(
FileInterceptor('image', {
storage: diskStorage({
destination: './uploads',
filename(_, file, callback) {
const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');
return callback(null, `${randomName}${extname(file.originalname)}`);
},
}),
})
)
What I would do here if code coverage is an absolute must on all lines with unit tests, is move the filename function to a separate file that exports the method so it can be tested directly.
During unit tests, enhancers aren't called, only created, so you won't get any coverage other than the initial "yeah, this decorator works" kind of coverage. For this inner callback, you'll need a separate function.
The other option, if code coverage isn't as important a metric, would be to test this in an e2e test. That way you know for certain this works as intended
My setup
Win10
Electron 2.0.1
I'm following an example from the book Electron in Action , Chapter 3, where the sample app loads a Markdown file into an Electron editor and shows it in HTML within a dual-pane view.
However, having followed the exact procedure and arrived at the exact same source code at the end of the chapter. I'm seeing nothing the author was trying to show.
Repro:
Run the project;
Open a .md file;
Notice that the two panes are blank.
I'm pretty new to this and fail to see anything useful from the console. So if anyone could code-review the following short listings it'd be much appreciated!
Here is the main.js
const { app, BrowserWindow, dialog } = require('electron');
const fs = require('fs');
let mainWindow = null;
app.on('ready', () => {
console.log('Hello from Electron');
mainWindow = new BrowserWindow({
show: false, // Delay showing window to avoid initial blank.
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadFile('./index.html'); // relpath = same folder as main.js
mainWindow.once('ready-to-show', () => {
mainWindow.show();
})
mainWindow.on('closed', () => {
mainWindow = null;
});
});
const getFileFromUser = exports.getFileFromUser = () => {
const files = dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
});
if (files) { openFile(files[0]); }
};
const openFile = (file) => {
const content = fs.readFileSync(file).toString();
mainWindow.webContents.send('file-opened', file, content);
}
Here is renderer.js
const { remote, ipcRenderer } = require('electron');
const mainProcess = remote.require('./main.js'); // plug in main process
const marked = require('marked'); // import marked as marked
// From document(index.html), find and refer to section '#markdown'
const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default');
const renderMarkdownToHtml = (markdown) => {
htmlView.innerHTML = marked(markdown, { sanitize: true });
};
markdownView.addEventListener('keyup', (event) => {
const currentContent = event.target.value;
renderMarkdownToHtml(currentContent);
});
openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser();
});
ipcRenderer.on('file-opened', (event, file, content) => {
markdownView.value = content;
renderMarkdownToHtml(content);
});
UPDATE
I opened the Developer Tool inside the app window and saw this error
remote.js:221 Uncaught Error: Could not call remote method 'getFileFromUser'. Check that the method signature is correct. Underlying error: The "path" argument must be one of type string, Buffer, or URL. Received type undefined
Underlying stack: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be one of type string, Buffer, or URL. Received type undefined
at Object.openSync (fs.js:439:10)
at Object.func (electron/js2c/asar.js:140:31)
at Object.func [as openSync] (electron/js2c/asar.js:140:31)
at Object.readFileSync (fs.js:349:35)
at Object.fs.readFileSync (electron/js2c/asar.js:542:40)
at Object.fs.readFileSync (electron/js2c/asar.js:542:40)
at openFile (path\to\hello_electron\app\main.js:36:24)
at Object.exports.getFileFromUser (path\to\hello_electron\app\main.js:32:18)
at electron/js2c/browser_init.js:6620:63
at EventEmitter.<anonymous> (electron/js2c/browser_init.js:6473:21)
at electron/js2c/browser_init.js:6622:17
at EventEmitter.<anonymous> (electron/js2c/browser_init.js:6473:21)
at EventEmitter.emit (events.js:203:13)
at WebContents.<anonymous> (electron/js2c/browser_init.js:3845:23)
at WebContents.emit (events.js:203:13)
path\to\hello_electron\node_modules\marked\lib\marked.js:1541 marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options
checkSanitizeDeprecation # path\to\hello_electron\node_modules\marked\lib\marked.js:1541
But looking at the main.js code above, I can't find why the path is invalid.
UPDATE 2
I found that the book sample was expecting a different returned value of showOpenDialog from my version.
My API doc says about showOpenDialog
/**
* Resolve with an object containing the following:
*
* * `canceled` Boolean - whether or not the dialog was canceled.
* * `filePaths` String[] - An array of file paths chosen by the user. If the
* dialog is cancelled this will be an empty array.
* * `bookmarks` String[] (optional) _macOS_ _mas_ - An array matching the
* `filePaths` array of base64 encoded strings which contains security scoped
* bookmark data. `securityScopedBookmarks` must be enabled for this to be
* populated.
*
* The `browserWindow` argument allows the dialog to attach itself to a parent
* window, making it modal.
*
* The `filters` specifies an array of file types that can be displayed or selected
* when you want to limit the user to a specific type. For example:
*
* The `extensions` array should contain extensions without wildcards or dots (e.g.
* `'png'` is good but `'.png'` and `'*.png'` are bad). To show all files, use the
* `'*'` wildcard (no other wildcard is supported).
*
* **Note:** On Windows and Linux an open dialog can not be both a file selector
* and a directory selector, so if you set `properties` to `['openFile',
* 'openDirectory']` on these platforms, a directory selector will be shown.
*/
So I switched my code to use files.filePaths, but got new errors
remote.js:221 Uncaught Error: Could not call remote method 'getFileFromUser'. Check that the method signature is correct. Underlying error: Cannot read property '0' of undefined
Underlying stack: TypeError: Cannot read property '0' of undefined
at Object.exports.getFileFromUser (path\to\hello\app\main.js:37:29)
at electron/js2c/browser_init.js:6620:63
at EventEmitter.<anonymous> (electron/js2c/browser_init.js:6473:21)
at EventEmitter.emit (events.js:203:13)
at WebContents.<anonymous> (electron/js2c/browser_init.js:3845:23)
at WebContents.emit (events.js:203:13)
at electron/js2c/browser_init.js:6622:17
at EventEmitter.<anonymous> (electron/js2c/browser_init.js:6473:21)
at EventEmitter.emit (events.js:203:13)
at WebContents.<anonymous> (electron/js2c/browser_init.js:3845:23)
at WebContents.emit (events.js:203:13)
OK, solved it myself. It seems that the book was using the showOpenDialog API according to a wrong/outdated syntax. It is likely that the API used to be synchronous, but later became async by default. I had to replace the sample code
const getFileFromUser = exports.getFileFromUser = () => {
const files = dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
});
if (files) { openFile(files[0]); }
};
with an async version
const getFileFromUser = exports.getFileFromUser = () => {
dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
}).then(result => {
if (result.filePaths.length > 0) { openFile(result.filePaths[0]); }
}).catch(err => {
console.log(err);
})
};
Also the returned promise contains more content than the book sample expects.
After this fix, I can see the loaded Markdown file in the panes as expected.
Sentry by defaults has integration for console.log to make it part of breadcrumbs:
Link: Import name: Sentry.Integrations.Console
How can we make it to work for bunyan logger as well, like:
const koa = require('koa');
const app = new koa();
const bunyan = require('bunyan');
const log = bunyan.createLogger({
name: 'app',
..... other settings go here ....
});
const Sentry = require('#sentry/node');
Sentry.init({
dsn: MY_DSN_HERE,
integrations: integrations => {
// should anything be handled here & how?
return [...integrations];
},
release: 'xxxx-xx-xx'
});
app.on('error', (err) => {
Sentry.captureException(err);
});
// I am trying all to be part of sentry breadcrumbs
// but only console.log('foo'); is working
console.log('foo');
log.info('bar');
log.warn('baz');
log.debug('any');
log.error('many');
throw new Error('help!');
P.S. I have already tried bunyan-sentry-stream but no success with #sentry/node, it just pushes entries instead of treating them as breadcrumbs.
Bunyan supports custom streams, and those streams are just function calls. See https://github.com/trentm/node-bunyan#streams
Below is an example custom stream that simply writes to the console. It would be straight forward to use this example to instead write to the Sentry module, likely calling Sentry.addBreadcrumb({}) or similar function.
Please note though that the variable record in my example below is a JSON string, so you would likely want to parse it to get the log level, message, and other data out of it for submission to Sentry.
{
level: 'debug',
stream:
(function () {
return {
write: function(record) {
console.log('Hello: ' + record);
}
}
})()
}
I am trying to work on a custom jasmine reporter and get a list of all the failed specs in the specDone function:
specDone: function(result) {
if(result.status == 'failed') {
failedExpectations.push(result.fullName);
console.log(failedExpectations);
}
}
where failedExpectations will store an entire list of the failed specs and i need to access this in the afterLaunch function in the protractor config file. But due to the fact that the config file loads everytime a new spec runs it basically gets overwritten and scoping is such that I cannot access it in the afterLaunch function, that is where I am making the call to the slack api. Is there a way to achieve this?
This is what i have it based on : http://jasmine.github.io/2.1/custom_reporter.html
I think the best way is to post the results asynchronously after each spec (*or every "it" and "describe") using #slack/web-api. This way you don't have to worry about overwriting. Basically you "collect" all the results during the test run and send it before the next suite starts.
Keep in mind all of this should be done as a class.
First you prepare your you '#slack/web-api', so install it (https://www.npmjs.com/package/#slack/web-api).
npm i -D '#slack/web-api'
Then import it in your reporter:
import { WebClient } from '#slack/web-api';
And initialize it with your token. (https://slack.com/intl/en-pl/help/articles/215770388-Create-and-regenerate-API-tokens):
this.channel = yourSlackChannel;
this.slackApp = new WebClient(yourAuthToken);
Don't forget to invite your slack app to the channel.
Then prepare your result "interface" according to your needs and possibilities. For example:
this.results = {
title: '',
status: '',
color: '',
successTests: [],
fails: [],
};
Then prepare a method / function for posting your results:
postResultOnSlack = (res) => {
try {
this.slackApp.chat.postMessage({
text: `Suit name: ${res.title}`,
icon_emoji: ':clipboard:',
attachments: [
{
color: res.color,
fields: [
{
title: 'Successful tests:',
value: ` ${res.successTests}`,
short: false
},
{
title: 'Failed tests:',
value: ` ${res.fails}`,
short: false
},
]
}
],
channel: this.channel
});
console.log('Message posted!');
} catch (error) {
console.log(error);
}
When you got all of this ready it's time to "collect" your results.
So on every 'suitStart' remember to "clear" the results:
suiteStarted(result) {
this.results.title = result.fullName;
this.results.status = '';
this.results.color = '';
this.results.successTests = [];
this.results.fails = [];
}
Then collect success and failed tests:
onSpecDone(result) {
this.results.status = result.status
// here you can push result messages or whole stack or do both:
this.results.successTests.push(`${test.passedExpectations}`);
for(var i = 0; i < result.failedExpectations.length; i++) {
this.results.fails.push(test.failedExpectations[i].message);
}
// I'm not sure what is the type of status but I guess it's like this:
result.status==1 ? this.results.color = #DC143C : this.results.color = #048a04;
}
And finally send them:
suiteDone() {
this.postResultOnSlack(this.results);
}
NOTE: It is just a draft based on reporter of mine. I just wanted to show you the flow. I was looking at Jasmine custom reporter but this was based on WDIO custom reporter based on 'spec reporter'. They are all very similar but you probably have to adjust it. The main point is to collect the results during the test and send them after each part of test run.
*You can look up this explanation: https://webdriver.io/docs/customreporter.html
I highly recommend this framework, you can use it with Jasmine on top.