I have a bunch of async functions, that I always or nearly always want to call synchronously. So we all know the pattern
async function somethingcool() {
return new Promise(resolve => {
setTimeout(resolve, 1000, "Cool Thing");
});
}
const coolthing = await somethingcool();
console.log(coolthing);
But I have this cool module called manycooolthings which offers many cool things, all via async functions that I always or nearly always want to await on.
import * as cool from 'manycoolthings';
await cool.updateCoolThings();
const coolThing = await cool.aCoolThing();
const anohtherCoolThing = await cool.anotherCoolThing();
const rus = await cool.coolThingsAreUs();
await cool.sendCoolThings();
await cool.postCoolThing(myCoolThing);
await cool.moreCoolThings();
const thingsThatAreCool = await cool.getThingsThatAreCool();
Extremely contrived and silly example, to illustrate the point. I do have a genuine use case, a set of tests based on puppeteer where most functions are async and they almost always want to be awaited on.
There must be a better way to avoid all the await pollution of our JavaScript code.
It would be great if could do something like
import * as cool from 'manycoolthings';
await {
cool.updateCoolThings();
const coolThing = cool.aCoolThing();
const anotherCoolThing = cool.anotherCoolThing();
const rus = cool.coolThingsAreUs();
cool.sendCoolThings();
cool.postCoolThing(myCoolThing);
cool.moreCoolThings();
const thingsThatAreCool = cool.getThingsThatAreCool();
}
Or even just
import * as cool from 'manycoolthings';
cool.updateCoolThings();
const coolThing = cool.aCoolThing();
const anotherCoolThing = cool.anotherCoolThing();
const rus = cool.coolThingsAreUs();
cool.sendCoolThings();
cool.postCoolThing(myCoolThing);
cool.moreCoolThings();
const thingsThatAreCool = cool.getThingsThatAreCool();
without having to worry if the method being called is async or not, because it's defined as an auto await function or something.
If you're unhappy with multiple awaits or thens, you can make a little "sequence" helper:
let _seq = async fns => fns.reduce((p, f) => p.then(f), Promise.resolve(null))
and use it like this:
result = await seq(
_ => cool.updateCoolThings(),
_ => _.aCoolThing(),
_ => _.anotherCoolThing(),
_ => _.coolThingsAreUs(),
)
which is almost your snippet #2.
Related
Consider this really simple example:
class MyClass {
public add(num: number): number {
return num + 2;
}
}
const result = await page.evaluate((NewInstance) => {
console.log("typeof instance", typeof NewInstance); // undefined
const d = new NewInstance();
console.log("result", d.add(10));
return d.add(10);
}, MyClass);
I've tried everything I could think of. The main reason I want to use a class here, is because there's a LOT of code I don't want to just include inside the evaluate method directly. It's messy and hard to keep track of it, so I wanted to move all logic to a class so it's easier to understand what's going on.
Is this possible?
It's possible, but not necessarily great design, depending on what you're trying to do. It's hard to suggest the best solution without knowing the actual use case, so I'll just provide options and let you make the decision.
One approach is to stringify the class (either by hand or with .toString()) or put it in a separate file, then addScriptTag:
const puppeteer = require("puppeteer"); // ^19.6.3
class MyClass {
add(num) {
return num + 2;
}
}
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.goto(
"https://www.example.com",
{waitUntil: "domcontentloaded"}
);
await page.addScriptTag({content: MyClass.toString()});
const result = await page.evaluate(() => new MyClass().add(10));
console.log(result); // => 12
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
See this answer for more examples.
Something like eval is also feasible. If it looks scary, consider that anything you put into a page.evaluate() or page.addScriptTag() is effectively the same thing as far as security goes.
const result = await page.evaluate(MyClassStringified => {
const MyClass = eval(`(${MyClassStringified})`);
return new MyClass().add(10);
}, MyClass.toString());
Many other patterns are also possible, like exposing your library via exposeFunction if the logic is Node-based rather than browser-based.
That said, defining the class inside an evaluate may not be as bad as you think:
const addTonsOfCode = () => {
MyClass = class {
add(num) {
return num + 2;
}
}
// ... tons of code ...
};
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.goto(
"https://www.example.com",
{waitUntil: "domcontentloaded"}
);
await page.evaluate(addTonsOfCode);
const result = await page.evaluate(() => new MyClass().add(10));
console.log(result); // => 12
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
I'd prefer to namespace this all into a library:
const addTonsOfCode = () => {
class MyClass {
add(num) {
return num + 2;
}
}
// ... tons of code ...
window.MyLib = {
MyClass,
// ...
};
};
Then use with:
await page.evaluate(addTonsOfCode);
await page.evaluate(() => new MyLib.MyClass().add(10));
(This seems partially overlapping with my previous question: Test interaction with two interval async functions)
Using jest 27.5.1 and sinon 13.0.1, I want to unit-test an async interval function.
Consider this code:
const fs = require("fs")
const { exec } = require("child_process")
module.exports = class MyClass {
init() {
setInterval(this.checkFileContent, 10)
}
async checkFileContent() {
await new Promise((resolve) => { exec("sleep 0.4", resolve) })
const fileContent = (await fs.promises.readFile("foo.txt")).toString()
console.log(fileContent)
}
}
and this test file
const MyClass = require(".")
const fs = require("fs")
const sinon = require("sinon")
const { exec } = require("child_process")
describe("MyClass", () => {
let sandbox
beforeAll(async () => {
sandbox = sinon.createSandbox()
await fs.writeFileSync("foo.txt", "fooContent")
})
afterAll(async () => {
await fs.unlinkSync("foo.txt")
await new Promise((resolve) => { exec("sleep 0.8", resolve) })
sandbox.verifyAndRestore()
})
test("should test MyClass", async () => {
const clock = sandbox.useFakeTimers()
const instance = new MyClass()
instance.init()
clock.next()
await Promise.resolve() // As suggested in this answer https://stackoverflow.com/a/52196951/2710714 - but doesn't seem to help at all
clock.reset()
})
})
Running the test, I'll always get ENOENT: no such file or directory, open 'foo.txt'. The reason being that await fs.unlinkSync("foo.txt") has already run when await fs.promises.readFile("foo.txt") is reached.
So, I need to somehow make sure that all promises in the interval function started by clock.next() are resolved before the tests stops. How do I do that?
Note: In real-life, it's not about file access but database queries. And the exec("sleep ..." is of course something artificial to replicate long-lasting operations. But the principles should be the same.
Edit: Also tried await clock.nextAsync() - didn't help.
If I have these lines:
const service = await getService('player');
const players = await service.players();
Can I use this one-liner version instead? Are they equivalent?
const players = await (await getService('player')).players();
Is there an even more streamlined way to write it?
Short answer: It's equivalent to in terms of result.
However, As #CertainPerformance's comment, you should use the first snippet to be able to debugger (as well as to follow best practice) like this.
let getService = (str) => new Promise(resolve => setTimeout(() => resolve({players: getPlayers}), 1000));
let getPlayers = () => new Promise(resolve => setTimeout(() => resolve("Response data"), 1000));
async function run() {
const service = await getService('player');
console.log(service); // debugger to watch response of each async func
const players = await service.players();
console.log(players); // debugger to watch response of each async func
}
run();
I'm just trying to understand the benefits of this:
const populateUsers = done => {
User.remove({}).then(async () => {
const userOne = new User(users[0]).save();
const userTwo = new User(users[1]).save();
const usersProm = await Promise.all([userOne, userTwo]).then(() => done());
return usersProm;
});
};
over this:
const populateUsers = done => {
User.remove({})
.then(() => {
const userOne = new User(users[0]).save();
const userTwo = new User(users[1]).save();
return Promise.all([userOne, userTwo]);
})
.then(() => done());
};
I came to this problem because eslint suggested my to use async in this function, and I remember the concept, make it work in my app, but I'm not sure why should I use this instead of the original way
Your original code was totally fine.
No, there is no benefit in using the code from your first snippet. You should avoid mixing await and .then(…) syntax! To use async/await, you'd make the whole function async, not the then callback:
async function populateUsers(done) {
await User.remove({})
const userOne = new User(users[0]).save();
const userTwo = new User(users[1]).save();
await Promise.all([userOne, userTwo]);
return done();
}
(Probably you would also remove that done callback - the function already returns a promise)
Your first version does not go all the way. Do this:
const populateUsers = done => {
User.remove({}).then(async () => {
const userOne = new User(users[0]).save();
const userTwo = new User(users[1]).save();
await Promise.all([userOne, userTwo]);
const usersProm = await done();
return usersProm;
});
};
There is no difference, it is just that code without these then callbacks is somewhat easier to read.
You might even apply it to the outer function:
const populateUsers = async () => {
await User.remove({});
const userOne = new User(users[0]).save();
const userTwo = new User(users[1]).save();
await Promise.all([userOne, userTwo]);
const usersProm = await done();
return usersProm;
};
Now populateUsers returns the promise instead of undefined.
As concluded in comments: you get an error because populateUsersreturns a promise and accepts a done callback argument, while one of these is expected, not both.
I'm a novice with Javascript and am struggling to understand how or at least, how best to return array values to another script to assert agains their values.
The context is I want to use Puppeteer to obtain some string values from WebElement attributes and then use the Chai expect library to assert for correct values( or otherwise).
The code I have thus far is:
//app.spec.js
const clothingChoice = await frame.$eval('#option-clothing-5787', e => e.getAttribute('value'));
const groceryChoice = await frame.$eval('#option-clothing-4556', e => e.getAttribute('value'));
const wineChoice = await frame.$eval('#option-clothing-4433', e => e.getAttribute('value'));
const voucherChoice = await frame.$eval('#option-clothing-3454', e => e.getAttribute('value'));
function testFunction() {
return new Promise(function(resolve, reject) {
resolve([clothingChoice, groceryChoice, wineChoice, voucherChoice]);
});
}
async function getChosenItemValues() {
const [clothingChoice, groceryChoice, wineChoice, voucherChoice] = await testFunction();
console.log(clothingChoice, groceryChoice, wineChoice, voucherChoice);
}
getChosenItemValues();
module.exports = getChosenItemValues;
};
I simply need to understand how to import the values that are currently simply printed out as:
1|clothing|option 1|grocery|option 1|wine|option 1|voucher|option
...into another file test.js in which I want to use chai to assert for their presence like so:
const [clothingEmailGrantedValue,emailRewardsNewsletterGrantedValue, emailGroceryOffersGrantedValue,telephoneRewardsDeniedValue ] = await app.spec.js(page, frame);
expect(clothingChoice).to.equal('1|clothing|option');
One way of doing this would be to export directly your function and destructure your array inside your test.js file.
async function getChosenItemValues() {
return await testFunction();
}
module.exports = getChosenItemValues()
inside your test.js file
const [clothingChoice, groceryChoice, wineChoice, voucherChoic] = require('./app.spec.js');
console.log(clothingChoice, groceryChoice, wineChoice, voucherChoice);