I've in the past couple of months used Puppeteer to drive an automation for a couple of small level projects. Now I want to scale the framework for a medium/large complex application.
I want to use the famed Page Object Model, where in I have separated the locators, page methods in separate files and I'm calling them in the corresponding page execution code.
My directory structure is like this
e2e_tests
- locators
- common-locators.js
- page1locators.js
- page2locators.js
- constants
- config.js
- utils
- base_functions.js
- page1methods.js
- page2methods.js
- urls
- urls.json
- screenshots
- test
- bootstrap.js
- page1.js
- page2.js
The problem I'm facing right now is that I am not able to get the page to initialise in the method body for that particular page.
For e.g. if I have an input box in page1, I want to define a method inside utils/page1methods.js which can take care of this - something like
module.exports = {
fillFirstInputBox(){
await page.type(locator, "ABCDEFG");
}
}
And then I want to call this inside the page1.js it block - something like this
const firstPage = require('../utils/page1methods.js').
.
.
.
it('fills first input box', async function (){
firstPage.fillFirstInputBox();
});
I've tried this approach and ran into all kinds of .js errors regarding page being not defined in the page1methods.js file. I can copy paste the errors if that's necessary.
What can I do so that I
I am able to achieve this kind of modularisation.
If I can improve on this structure, what should be my approach.
You can return an arrow function that will return the modules/set of functions with page variable. Be sure to wrap the whole thing in first braces, or manually return it.
module.exports = (page) => ({ // <-- to have page in scope
async fillFirstInputBox(){ // <-- make this function async
await page.type(locator, "ABCDEFG");
}
})
And then pass the variable up there,
// make page variable
const firstPage = require('../utils/page1methods.js')(page)
That's it. Now all functions have access to page variable. There are other ways like extending classes, binding page etc. But this will be the easiest way as you can see. You can split it if you need.
We are halfway there. That itself won't solve this problem. The module still won't work due to async-await and class issue.
Here is a full working example,
const puppeteer = require("puppeteer");
const extras = require("./dummy"); // call it
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto("https://www.example.com");
const title = await extras(page).getTitle(); // use it here
console.log({ title }); // prints { title: 'Example Domain' }
await browser.close();
});
Related
This answer to a similar question does a great job at explaining how fastify-plugin works and what it does. After reading the explanation, I still have a question remaining; how is this different from a normal function call instead of using the .register() method?
To clarify with an example, how are the two approaches below different from each other:
const app = fastify();
// Register a fastify-plugin that decorates app
const myPlugin = fp((app: FastifyInstance) => {
app.decorate('example', 10);
});
app.register(myPlugin);
// Just decorate the app directly
const decorateApp = (app: FastifyInstance) => {
app.decorate('example', 10);
};
decorateApp(app);
By writing a decorateApp function you are creating your own "API" to load your application.
That said, the first burden you will face soon is sync or async:
decorateApp is a sync function
decorateAppAsync within an async function
For example, you need to preload something from the database before you can start your application.
const decorateApp = (app) => {
app.register(require('#fastify/mongodb'))
};
const businessLogic = async (app) => {
const data = await app.mongo.db.collection('data').find({}).toArray()
}
decorateApp(app)
businessLogic(app) // whoops: it is async
In this example you need to change a lot of code:
the decorateApp function must be async
the mongodb registration must be awaited
the main code that loads the application must be async
Instead, by using the fastify's approach, you need to update only the plugin that loads the database:
const applicationConfigPlugin = fp(
+ async function (fastify) {
- function (fastify, opts, next) {
- app.register(require('#fastify/mongodb'))
- next()
+ await app.register(require('#fastify/mongodb'))
}
)
PS: note that fastify-plugin example code misses the next callback since it is a sync function.
The next bad pattern will be high hidden coupling between functions.
Every application needs a config. Usually, the fastify instance is decorated with it.
So, you will have something like:
decorateAppWithConfig(app);
decorateAppWithSomethingElse(app);
Now, decorateAppWithSomethingElse will need to know that it is loaded after decorateAppWithConfig.
Instead, by using the fastify-plugin, you can write:
const applicationConfigPlugin = fp(
async function (fastify) {
fastify.decorate('config', 42);
},
{
name: 'my-app-config',
}
)
const applicationBusinessLogic = fp(
async function (fastify) {
// ...
},
{
name: 'my-app-business-logic',
dependencies: ['my-app-config']
}
)
// note that the WRONG order of the plugins
app.register(applicationBusinessLogic);
app.register(applicationConfigPlugin);
Now, you will get a nice error, instead of a Cannot read properties of undefined when the config decorator is missing:
AssertionError [ERR_ASSERTION]: The dependency 'my-app-config' of plugin 'my-app-business-logic' is not registered
So, basically writing a series of functions that use/decorate the fastify instance is doable but it adds
a new convention to your code that will have to manage the loading of the plugins.
This job is already implemented by fastify and the fastify-plugin adds many validation checks to it.
So, by considering the question's example: there is no difference, but using that approach to a bigger application
will lead to a more complex code:
sync/async loading functions
poor error messages
hidden dependencies instead of explicit ones
Importing a long JS function from a different file:
let startTheShow = require('./scraper.js');
And then trying to use that function on the page.
await page.evaluate(() => {
startTheShow('info','hi','hi');
})
What is the expected result?
The imported function will run on the page
What happens instead?
"startTheShow is not defined"
Placing the raw script inside the evaluate instead of importing the module to the index, fixes the issue. Unfortunately this option makes a lot of mess in the index file. Any idea why I can't use this imported function?
From the docs:
pageFunction <function|string> Function to be evaluated in the page context
evaluate(() => startTheShow()) means there should be a startTheShow function in the page context which I'd assume there isn't any and the page context has no idea what startTheShow is. You should either pass the string version of ./scraper.js, or the startTheShow function to evaluate.
Example of passing strings from docs:
console.log(await page.evaluate('1 + 2')); // prints "3"
I have a protractor-cucumber framework whose step definitions are somewhat structured as per this: https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/step_definitions.md
I use a return and chain the promises together. Recently, I came across a different syntax called the async function. But, when I try to convert my step definitions to async, all the help files in the framework where I use say module.exports and require() display the following warning:
[ts] File is a CommonJS module; it may be converted to an ES6 module.
When I run test cases since I can't access these helper files due to the error my tests cases fail. Like, my page object files, I am not able to access them from my tests. I think they don't get exported like they used to.
Could someone please advice me as to how I can change my test cases to async syntax without breaking them? How do I resolve the above issue without disrupting my tests in a major way.
Adding code
Here is a step from my step definition before the change
let { Given, Then, When } = require('cucumber');
Given(/^I am on the "([^"]*)" page$/, function (home) {
home = this.url.FDI_HOME;
return browser.get(home);
});
Here is a step definition, after I change it to an async function
let { Given, Then, When } = require('cucumber');
Given(/^I am on the "([^"]*)" page$/, async function (home) {
home = this.url.HOME
await browser.get(home);
});
And I will change my other steps in similar fashion. Problem arises when I try to run the above step it fails saying that it is not able to access this.url.HOME. I have another file to supply URLs called the urls.js looks something like this
let targetStore = browser.params.store || 'bestbuy';
let FDI_HOST = browser.params.fdi;
module.exports = {
HOME Page: 'https://homepage.com',
Shop_Page: 'https://shop.com',
storeLink: `http://www.${targetStore}.com`,
};
I see three dots under the word "module.exports" in VS code and when I hover over it, it displays an error saying: [ts] File is a CommonJS module; it may be converted to an ES6 module.
I have tried to find a resolution to this but not been able to successfully make it. if I use the syntax as "async()=>{}" the test cases fails but when I use "async function(){}" then a few of the steps pass but not the other.
These are suggestions/hints. They visually indicate that vscode can perform an action to possibly refactor/improve your code, but they are not treated as errors.
You can disable them by adding "javascript.suggestionActions.enabled": false to your user/workspace settings.
Source: https://github.com/Microsoft/vscode/issues/47299
Update
I've completely rewritten this question based on subsequent investigation. Hopefully this will generate some answers.
I'm new to Postman, and trying to figure out how to most efficiently build a collection of tests for a REST application. There are a bunch of utility functions that I'd like to have accessible in each of my test scripts, but cut-and-paste-ing them in to each test script seems like a horrible solution.
In looking at the various "scopes" that Postman allows you to squirrel away data (e.g. globals, environment, collection), it seems that all of these are merely string/number stores. In other words, it properly stores them if you can/do stringify the results. But it doesn't actually allow you to store proper objects or functions. This makes sense, since each script seems to be run as a separate execution, so the idea of sharing pointers to things between different scripts doesn't make sense.
It seems like the accepted way to share utility functions is to toString() the function in the defining script (e.g. the Collection Pre-Req script), and then eval() that stringified version in the test script. For instance:
Collection Pre-Req Script
const utilFunc = () => { console.log("I am a utility function"); };
pm.environment.set("utilFunc",utilFunc.toString() );
Test Script
const utilFunc = eval(pm.environment.get("utilFunc"));
utilFunc();
The test script will successfully print to console "I am a utility function".
I've seen people do more complicated things where, if they have more than one utility function, put them in to an object like utils.func1 and utils.func2, and have the overall function return the utils object, so the test script still only has to have a single line at the top importing the whole thing.
The problem I'm running in to is scoping - since the literal text of the function is executed in the Test Script, everything thing that the utility function has to have must be in that code, or otherwise exist at eval() time in the Test Script. For instance, if I do:
Collection Pre-Req Script
const baseUtilFunc = (foo) => { console.log(foo); };
const utilFunc1 = (param) => { baseUtilFunc("One: " + param); };
const utilFunc2 = (param) => { baseUtilFunc("Two: " + param); };
pm.environment.set("utilFunc1",utilFunc1.toString() );
pm.environment.set("utilFunc2",utilFunc2.toString() );
Test Script
const utilFunc1 = eval(pm.environment.get("utilFunc1"));
const utilFunc2 = eval(pm.environment.get("utilFunc2"));
utilFunc1("Test");
This fails because, in the Test Script, baseUtilFunc does not exist. Obviously, in this example, it'd be easy to fix. But in a more complicated world where the utility functions I expect to use in my Test Scripts are themselves built on top of underlying helper functions, it gets more difficult.
So what is the right way to handle this issue? Do people just cram all the relevant logic in to one big function that they then call toString() on? Do they embed an extraction-from-environment-and-then-eval in each util function within its definition, so that it works in the Test Script context? Do they export each individual method?
There are different ways to do it. The way I did recently for one of the projects is creating a project in Git and then using raw url to fetch the data. I have a sample created at below repo
https://github.com/tarunlalwani/postman-utils
To load the file you will need to associate the below code at collection level
if (typeof pmutil == "undefined") {
var url = "https://raw.githubusercontent.com/tarunlalwani/postman-utils/master/pmutils.js";
if (pm.globals.has("pmutiljs"))
eval(pm.globals.get("pmutiljs"))
else {
console.log("pmutil not found. loading from " + url);
pm.sendRequest(url, function (err, res) {
eval(res.text());
pm.globals.set('pmutiljs', res.text())
});
}
}
As shown in below screenshot
And the later in the tests or Pre-Requests you will run the below line of code to load it
eval(pm.globals.get("pmutiljs"))
And then you can use the functions easily in test.
Previously using Nightwatch.js I was able to create custom Nightwatch commands: https://github.com/nightwatchjs/nightwatch-docs/blob/master/guide/extending-nightwatch/custom-commands.md
I'm wondering if there is anything that exists like this for Puppeteer-- the closest thing I've seen is: Is there a way to add script to add new functions in evaluate() context of chrome+puppeeter?
But it's still far away from what I want. I would like to be able to call page.commonAction(...) instead of
page.x();
page.y();
page.z();
You can always create your own script with functions you can call. For example, I have a myFunctions.js in the same folder, from which I name
const mf = require('./myFunctions.js');
Inside the myFunctions.js I have, for example, this function:
async function verifyElementPresent(page, selector) {
let verifySelector = await page.$(selector);
if (verifySelector !== null) {
console.log(found('> OK - Element present'));
} else {
console.log(notfound('>>> ERROR - Element not present: ' + selector));
}
}
So now, in my Puppeteer script all I have to do is write:
mf.verifyElementPresent(page, '#headerTitle');
And it'll print in console the result.
Hope this helps.