puppeteer evaluate results in context destroyed - node.js

Trying to wait for DOM mutations to stop but ends with Execution context was destroyed., any suggestion is welcome
page.evaluate(() => {
return new Promise((resolve,reject) => {
var timerId;
function resetTimer() {
clearInterval(timerId);
timerId = setTimeout(() => {
resolve(true);
}, 3000)
}
new MutationObserver(
() => {
resetTimer();
}
).observe(document.getElementById('root'), {
attributes: true, childList: true
});
resetTimer();
})
})
})
Protocol error (Runtime.callFunctionOn): Execution context was destroyed. undefined
at Promise (node_modules/puppeteer/lib/Connection.js:198:56)
at CDPSession.send (node_modules/puppeteer/lib/Connection.js:197:12)
at ExecutionContext.evaluateHandle (node_modules/puppeteer/lib/ExecutionContext.js:71:75)
at ExecutionContext.evaluate (node_modules/puppeteer/lib/ExecutionContext.js:46:31)
at Frame.evaluate (node_modules/puppeteer/lib/FrameManager.js:299:20)

The above snippet was run before getting a navigating lock on a page. Running the page.evaluate between navigation can throw this error.
Found this from,
Error: Protocol error (Runtime.callFunctionOn): Execution context was destroyed.
No page navigation lock ?
Fix was (at least in my case) to wait till URL changed and then page.evaluate.

Notes:
Are you using async/await in your code? If you have a promise but don't use any chain or async/await, then it should throw up errors like that.
Also you are firing the resetTimer out of observer callback too.
If not, then you are probably not monitoring the correct changes of that dom element.
Here is a a simple react app which changes state after 2 seconds.
class App extends Component {
constructor() {
super();
this.state = { bar: "foo" };
}
componentDidMount() {
setTimeout(() => this.setState({ bar: "not foo" }), 2000);
}
render() {
return <div className="App">{this.state.bar}</div>;
}
}
Here is the modified code for the above snippet.
await page.evaluate(() => { // wait for it to resolve
return new Promise((resolve, reject) => {
var timerId;
function resetTimer() {
clearInterval(timerId);
timerId = setTimeout(() => {
resolve(true);
}, 5000); // resolves after some time
}
const observer = new MutationObserver((mutations) => {
// show me the changes
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
// reset timer etc.
resetTimer();
});
// observe a lot of changes
observer.observe(document.getElementById("root"), {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
});
});
Here is the result:

Related

Why does Promise.race not resolve in kafkajs eachMessage callback

I have defined a promise like this ...
const result = await Promise.race([
new Promise(resolve => {
consumer.run({
eachMessage: ({ message }) => {
const data = JSON.parse(message.value.toString());
if (data.payload.template
&& data.payload.template.id === '...'
&& data.payload.to[0].email === email) {
console.log('Should resolve!')
resolve(data.payload.template.variables.link);
console.log('resolved');
consumer.pause();
consumer.disconnect();
}
},
});
}),
new Promise((_, reject) => setTimeout(reject, 3000))
]);
console.log('result is ', result);
return result;
I can get to resolved but it doesnt print the result at the end, it seems like neither did the timeout nor the actual promise work as expected? Why is that? I suspect its something to do with using resolve inside the kafka js callback?
UPDATE: Seems like its Promise.race() thats not resolving, but why?
My suspicion is that your "success-side" promise inadvertently throws and you're swallowing the error silently.
Using a mock-up minimal implementation of the consumer (that succeeds or fails 50/50), the following code works.
Run the code sample a couple of times to see both cases.
var consumer = {
interval: null,
counter: 0,
run: function (config) {
this.interval = setInterval(() => {
this.counter++;
console.log(`Consumer: message #${this.counter}`);
config.eachMessage({message: this.counter});
}, 250);
},
pause: function () {
console.log('Consumer: paused');
clearInterval(this.interval);
},
disconnect: function () {
console.log('Consumer: disconnected');
}
};
Promise.race([
new Promise(resolve => {
const expectedMsg = Math.random() < 0.5 ? 3 : 4;
consumer.run({
eachMessage: ({ message }) => {
if (message === expectedMsg) resolve("success");
}
});
}),
new Promise((_, reject) => setTimeout(() => {
reject('timeout');
consumer.pause();
consumer.disconnect();
}, 1000))
]).then((result) => {
console.log(`Result: ${result}`);
}).catch((err) => {
console.log(`ERROR: ${err}`);
});
I have also moved consumer.pause() and consumer.disconnect() to the "timeout-side" promise, this way the consumer is guaranteed to disconnect, albeit it might run a tiny bit longer than necessary in the success case.

console log value in the on ready sequelize do not change

I'm trying to get a value in sequelize that I increment by 1 in a setInterval but when I try to do that on the ready event it returns the same value every time.
I tried doing it in a counter ( setInterval ) outside the listener (ready event) and it works fine.
client.once("ready", async () => {
console.log("Ready!");
Tags.sync();
client.channels.map((x) => {
if (x.type === "voice") {
x.members.map(async (y) => {
const user = await Tags.findOne({
where: { id: ID },
});
user.increment("rank");
setTimeout(() => {
console.log("start");
setInterval(async () => {
//Here the value is always the same
console.log("Test", user.get("level"));
user.increment("rank");
}, millisPerHour);
}, millisToTheHour);
});
}
});
});
How can I fix it? which could be a workaround for this "behaviour"?

How to detect a alert in puppeteer

How can I detect if after a navigation the page shows an alert with some message.
Can puppeteer detect or identify if the page has shown an alert box.
I tried
page.on('popup', ()=> {
console.log('popup detected');
});
also,
page.on('dialog', ()=> {
console.log('popup detected');
});
As said in the comments: If you register the event handlers (your code) after calling page.goto the event is already triggered before you are listening for the event.
Therefore, make sure to call page.goto after your code.
Here are my two cents :
private async gotoPage(path):Promise<Response|Result> {
return new Promise(async(resolve, reject) => {
await Promise.all([
this.page.on('dialog', async (dialog) => {
await dialog.dismiss().catch(() => {
console.log(dialog.message());
return new Result(TestStatus.FAIL, dialog.message());
});
}),
this.page.goto(`${this.baseURL}${path}`),
this.page.waitForNavigation({ waitUntil: 'load' })]).then(
async () => {
resolve(new Result(TestStatus.PASS, `Loading to path success : ${path}`));
},
async () => {
reject(new Result(TestStatus.FAIL, `Could not GotoPage : ${path}`));
});
});
}

NodeJS EventEmitter test with Mocha

I have a problem testing ldapjs client search operation. It returns an EventEmitter that you have to make listen for some specific event. I wrapped this operations to promisify it and to define my logic and I would like to unit-test it.
findUser(username) {
return new Promise((resolve, reject) => {
logger.debug('Searching user: ', username);
this.ldapClient.bind(user.name, .user.password, err => {
if (err) return reject(err);
else
this.ldapClient.search(root, {filter: `(cn=${username})`}, (errSearch, resSearch) => {
if (errSearch) return reject(errSearch);
const entries = [];
resSearch.on('searchEntry', entry => entries.push(entry.object));
resSearch.on('searchReference', referral => reject(new Error(`Received search referall: ${referral}`)));
resSearch.on('error', err => reject((err));
resSearch.on('end', result => {
if (result.status === 0 && entries.length === 1) {
return resolve({
cn: entries[0].cn,
objectclass: entries[0].objectclass,
password: entries[0].password
});
} else {
return reject(new Error(`Wrong search result: ${result}`));
}
});
});
});
});
}
I am using mockery and Sinon to replace ldapjs dependency inside my module:
beforeEach(function () {
searchEM = new EventEmitter();
sandbox = sinon.createSandbox();
ldapClientStub = Stubs.getLdapClientStub(sandbox);
ldapClientStub.bind.yields(null);
ldapClientStub.search.withArgs('o=ldap', {filter: `(cn=${findParam})`}).yields(null, searchEM);
mockery.registerMock('ldapjs', Stubs.getLdapStub(ldapClientStub));
mockery.registerAllowable('../src/client');
UserClientCls = require('../src/client').default;
userClient = new UserClientCls(config.get());
});
it('should return user with given username', function (done) {
setTimeout(() => {
searchEM.emit('searchEntry', users[1]);
searchEM.emit('end', {status: 0});
console.log('emitted');
}, 500);
searchEM.on('end', res => console.log(res));
userClient.findUser(findParam)
.then(user => {
user.cn.should.equal(users[1].attributes.cn);
user.objectclass.should.equal(users[1].attributes.objectclass);
user.password.should.equal(users[1].attributes.password);
return done();
})
.catch(err => done(err));
});
The problem is that listeners defined inside findUser are never called (but the function itself is called). The listener I defined in the test (just to debug the behaviour) is correctly called.
I do not understand if I miss something about how EventEmitters works or if I am doing the test in a wrong way. Or maybe I wrote a bad piece of code that cannot be tested.
I found a solution to my problem. I extended the base EventEmitter: I added the logic to store which event I want to emit and overrode its on method with a logic to emit my fake event.
class TestEventEmitter extends EventEmitter {
constructor() {
super();
}
setFakeEmit(fakeEmit) {
this.fakeEmit = fakeEmit;
}
on(eventName, cb) {
super.on(eventName, cb);
if (super.eventNames().length === 4)
this.fakeEmit.forEach(f => this.emit(f.name, f.obj));
}
}
So, in beforeEach I can stub ldapClientStub.search to make it return my TestEventEmitter:
beforeEach(function() {
searchEM = new TestEventEmitter();
searchEM.setFakeEmit([{
name: 'searchEntry',
obj: { object: users[1].attributes }
}, {
name: 'end',
obj: { status: 0 }
}]);
...
ldapClientStub.search.withArgs('o=ldap', { filter: `(&(cn=${findParam})(objectclass=astsUser))` }).yields(null, searchEM);
})
This solution may be not very elegant, but it works. If someone can post a better solution I'll be glad to have a look.

How to hold a NodeJS application until other promise completes

Using promises with NodeJS, I load a model that can then be re-used by susequent calls to the NodeJS app. How can I prevent the same object/model being loaded twice from a database if a second request arrives while the first is still being loaded?
I set a "loading flag" to say that the object is being retrieved from the database and "loaded" when done. If there is a second request that attempts to load the same object, it needs to wait until the initial model is filled and then both can use the same object.
Sample Code (simplified, ES6, Node 0.10 [old for a reason]).
It's the TODO that needs solving.
App:
import ClickController from './controllers/ClickController'
import express from 'express'
const app = express()
app.get('/click/*', (req, res) => {
// Get the parameters here
let gameRef = "test";
ClickController.getGameModel(gameRef)
.then(() => {
console.log('Got game model')
return this.handleRequest()
}, (err) => {
throw err
})
}
Controller:
import gameModel from '../models/GameModel'
class ClickController {
constructor(config) {
// Stores the objects so they can be re-used multiple times.
this.loadedGames = {}
}
// getGameModel() as a promise; return model (or throw error if it doesn't exist)
getGameModel(gameRef) {
return new Promise((resolve, reject) => {
let oGame = false
if(typeof this.loadedGames[gameRef] === 'undefined') {
oGame = new gameModel()
this.loadedGames[gameRef] = oGame
} else {
oGame = this.loadedGames[gameRef]
}
oGame.load(gameRef)
.then(function() {
resolve()
}, (err) => {
reject(err)
})
})
}
}
Model / Object:
class GameModel {
constructor {
this.loading = false
this.loaded = false
}
load(gameRef) {
return new Promise((resolve, reject) => {
if (this.loading) {
// TODO: Need to wait until loaded, then resolve or reject
} else if (!this.loaded) {
this.loading = true
this.getActiveDBConnection()
.then(() => {
return this.loadGame(gameRef)
}, (err) => {
console.log(err)
reject(err)
})
.then(() => {
this.loading = false
this.loaded = true
resolve()
})
} else {
// Already loaded, we're fine
resolve()
}
})
}
// As this uses promises, another event could jump in and call "load" while this is working
loadGame(gameRef) {
return new Promise((resolve, reject) => {
let sql = `SELECT ... FROM games WHERE gameRef = ${mysql.escape(gameRef)}`
this.dbConnection.query(sql, (err, results) => {
if (err) {
reject('Error querying db for game by ref')
} else if (results.length > 0) {
// handle results
resolve()
} else {
reject('Game Not Found')
}
})
})
}
}
I don't follow exactly which part of you're code you are asking about, but the usual scheme for caching a value with a promise while a request is already "in-flight" works like this:
var cachePromise;
function loadStuff(...) {
if (cachePromise) {
return cachePromise;
} else {
// cache this promise so any other requests while this one is stil
// in flight will use the same promise
cachePromise = new Promise(function(resolve, reject) {
doSomeAsyncOperation(function(err, result) {
// clear cached promise so subsequent requests
// will do a new request, now that this one is done
cachePromise = null;
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
return cachePromise;
}
}
// all these will use the same result that is in progress
loadStuff(...).then(function(result) {
// do something with result
});
loadStuff(...).then(function(result) {
// do something with result
});
loadStuff(...).then(function(result) {
// do something with result
});
This keeps a cached promise and, as long as request is "in-flight", the cachePromise value is in place and will be returned by subsequent requests.
As soon as the request actually finishes, the cachePromise will be cleared so that the next request that comes later will issue a new request.

Resources