Headless Chrome rendering full page - node.js

The problem with current headless Chrome is that there is no API to render the full page you only get the "window" that you set in CLI parameter.
I am using the chrome-remote-interface module, this is the capture example:
const fs = require('fs');
const CDP = require('chrome-remote-interface');
CDP({ port: 9222 }, client => {
// extract domains
const {Network, Page} = client;
Page.loadEventFired(() => {
const startTime = Date.now();
setTimeout(() => {
Page.captureScreenshot()
.then(v => {
let filename = `screenshot-${Date.now()}`;
fs.writeFileSync(filename + '.png', v.data, 'base64');
console.log(`Image saved as ${filename}.png`);
let imageEnd = Date.now();
console.log('image success in: ' + (+imageEnd - +startTime) + "ms");
client.close();
});
}, 5e3);
});
// enable events then start!
Promise.all([
// Network.enable(),
Page.enable()
]).then(() => {
return Page.navigate({url: 'https://google.com'});
}).catch((err) => {
console.error(`ERROR: ${err.message}`);
client.close();
});
}).on('error', (err) => {
console.error('Cannot connect to remote endpoint:', err);
});
To render the full page, one slower and hack solution would be partial rendering. Set fixed height and scroll through the page and take the screenshots after every X pixels. The problem is that how to drive the scrolling part? Would it be better to inject custom JS or is it doable through the Chrome remote interface?

Have you seen this?
https://medium.com/#dschnr/using-headless-chrome-as-an-automated-screenshot-tool-4b07dffba79a
This bit sound's like it would solve your issue:
// Wait for page load event to take screenshot
Page.loadEventFired(async () => {
// If the `full` CLI option was passed, we need to measure the height of
// the rendered page and use Emulation.setVisibleSize
if (fullPage) {
const {root: {nodeId: documentNodeId}} = await DOM.getDocument();
const {nodeId: bodyNodeId} = await DOM.querySelector({
selector: 'body',
nodeId: documentNodeId,
});
const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId});
await Emulation.setVisibleSize({width: viewportWidth, height: height});
// This forceViewport call ensures that content outside the viewport is
// rendered, otherwise it shows up as grey. Possibly a bug?
await Emulation.forceViewport({x: 0, y: 0, scale: 1});
}
setTimeout(async function() {
const screenshot = await Page.captureScreenshot({format});
const buffer = new Buffer(screenshot.data, 'base64');
file.writeFile('output.png', buffer, 'base64', function(err) {
if (err) {
console.error(err);
} else {
console.log('Screenshot saved');
}
client.close();
});
}, delay);
});

Chrome remote interface supports simulating scrolling gestures using the Input domain.
// scroll down y axis 9000px
Input.synthesizeScrollGesture({x: 500, y: 500, yDistance: -9000});
more info:
https://chromedevtools.github.io/devtools-protocol/tot/Input/
You may also be interested in the Emulation domain. dpd's answer contains a few now removed methods. I believe Emulation.setVisibleSize might work for you.
https://chromedevtools.github.io/devtools-protocol/tot/Emulation/

Related

Click anywhere on page using Puppeteer

Currently I'm using Puppeteer to fetch cookies & headers from a page, however it's using a bot prevention system which is only bypassed when clicking on the page; I don't want to keep this sequential so it's "detectable"
How can I have my Puppeteer click anywhere on the page at random? regardless of wether it clicks a link, button etc..
I've currently got this code
const getCookies = async (state) => {
try {
state.browser = await launch_browser(state);
state.context = await state.browser.createIncognitoBrowserContext();
state.page = await state.context.newPage();
await state.page.authenticate({
username: proxies.username(),
password: proxies.password(),
});
await state.page.setViewport(functions.get_viewport());
state.page.on('response', response => handle_response(response, state));
await state.page.goto('https://www.website.com', {
waitUntil: 'networkidle0',
});
await state.page.waitFor('.unlockLink a', {
timeout: 5000
});
await state.page.click('.unlockLink a');
await state.page.waitFor('input[id="nondevice"]', {
timeout: 5000
});
state.publicIpv4Address = await state.page.evaluate(() => {
return sessionStorage.getItem("publicIpv4Address");
});
state.csrfToken = await state.page.evaluate(() => {
return sessionStorage.getItem("csrf-token");
});
//I NEED TO CLICK HERE! CAN BE WHITESPACE, LINK, IMAGE
state.browser_cookies = await state.page.cookies();
state.browser.close();
for (const cookie of state.browser_cookies) {
if(cookie.name === "dtPC") {
state.dtpc = cookie.value;
}
await state.jar.setCookie(
`${cookie.name}=${cookie.value}`,
'https://www.website.com'
)
}
return state;
} catch(error) {
if(state.browser) {
state.browser.close();
}
throw new Error(error);
}
};
The simplest way I can think of out of my head to choose a random element from DOM would be probably something like using querySelectorAll() which will return you an array of all <div>s in your document (or choose any other element, like <p> or anything else), then you can easily use click() on random one from the result, for example:
await page.evaluate(() => {
const allDivs = document.querySelectorAll('.left-sidebar-toggle');
const randomElement = allDivs[Math.floor(Math.random() * allDivs.length)];
randomElement.click();
});

How to get Document.readyState in puppeteer / headless Chrome?

Using puppeteer, I cannot figure out how to get the document.readyState. I need to wait until the page is loaded before rending a pdf.
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
const page = await browser.newPage();
console.log('Setting HTML content...');
// Can't POST data with headless chrome, so we have to get the HTML and set the content of the page, then render that to a PDF
await page.setContent(html);
// Generates a PDF with 'screen' media type.
await page.emulateMedia('screen');
var renderPage = function () {
return new Promise(async resolve => {
await page.evaluate((document) => {
console.log(document);
const handleDocumentLoaded = () => {
console.log('readyState: ', document.readyState);
console.log('Rendering PDF...');
Promise.resolve(resolve(page.pdf({ path: thisPDFfileName, format: 'Letter' })));
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", handleDocumentLoaded);
} else {
handleDocumentLoaded();
}
});
// I also tried this... no luck
// setTimeout(async function () {
// console.log('Awaiting document...');
//
// const handle = await page.evaluateHandle(() => ({window, document}));
// const properties = await handle.getProperties();
// const windowHandle = properties.get('window');
// const documentHandle = properties.get('document');
// await handle.dispose();
//
// console.log('readyState: ', documentHandle.readyState);
// if ("complete" === documentHandle.readyState) {
// await documentHandle.dispose();
// console.log('readyState: ', doc.readyState);
// console.log('Rendering PDF...');
// resolve(page.pdf({ path: thisPDFfileName, format: 'Letter' }));
// } else {
// renderPage();
// }
// }), 250;
});
};
// Delay required to allow page to render JS before creating PDF
await renderPage();
await browser.close();
sendPdfToClient();
I tried evaluateHandle and could only get the innerHTML, not the document object itself.
What's the correct way to get the document object containing readyState?
Lastly, should I set a listener for loaded or DOMContentLoaded, I need to wait until the google maps JS renders the map? I can sent a custom event if need be, since I control the page being rendered.
If you are using page.goto(), you can use the waitUntil option to specify when to consider navigation as complete:
The waitUntil events include:
load - consider navigation to be finished when the load event is fired.
domcontentloaded - consider navigation to be finished when the DOMContentLoaded event is fired.
networkidle0 - consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.
networkidle2 - consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.
Alternatively, you can use page.on() to wait for the 'domcontentloaded' event or 'load' event.
I guess I was overcomplicated it. Apparently there is already
page.once('load', () => console.log('Page loaded!'));
which does exactly this. :-D
See detailed documentation here:
https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#event-load
There are 2 events which is relavant to your problem
event: 'domcontentloaded'
event 'load'

Using promises with PDFMake on Firebase Cloud Functions

I am using PDFMake (a variant of PDFKit) to generate PDFs on Firebase Cloud Functions using a realtime database trigger. The function gets all relevant data from the database and then passes it to the function that is supposed to generate the PDF.
All this is done using Promises. Everything works fine until the point where the PDF is actually generated.
Here's the code in my main event listener:
exports.handler = (admin, event, storage) => {
const quotationData = event.data.val();
// We must return a Promise when performing async tasks inside Functions
// Eg: Writing to realtime db
const companyId = event.params.companyId;
settings.getCompanyProfile(admin, companyId)
.then((profile) => {
return quotPdfHelper.generatePDF(fonts, profile, quotationData, storage);
})
.then(() => {
console.log('Generation Successful. Pass for email');
})
.catch((err) => {
console.log(`Error: ${err}`);
});
};
To generate the PDF, here's my code:
exports.generatePDF = (fonts, companyInfo, quotationData, storage) => {
const printer = new PdfPrinter(fonts);
const docDefinition = {
content: [
{
text: [
{
text: `${companyInfo.title}\n`,
style: 'companyHeader',
},
`${companyInfo.addr_line1}, ${companyInfo.addr_line2}\n`,
`${companyInfo.city} (${companyInfo.state}) - INDIA\n`,
`Email: ${companyInfo.email} • Web: ${companyInfo.website}\n`,
`Phone: ${companyInfo.phone}\n`,
`GSTIN: ${companyInfo.gst_registration_number} • PAN: AARFK6552G\n`,
],
style: 'body',
//absolutePosition: {x: 20, y: 45}
},
],
styles: {
companyHeader: {
fontSize: 18,
bold: true,
},
body: {
fontSize: 10,
},
},
pageMargins: 20,
};
return new Promise((resolve, reject) => {
// const bucket = storage.bucket(`${PROJECT_ID}.appspot.com`);
// const filename = `${Date.now()}-quotation.pdf`;
// const file = bucket.file(filename);
// const stream = file.createWriteStream({ resumable: false });
const pdfDoc = printer.createPdfKitDocument(docDefinition);
// pdfDoc.pipe(stream);
const chunks = [];
let result = null;
pdfDoc.on('data', (chunk) => {
chunks.push(chunk);
});
pdfDoc.on('error', (err) => {
reject(err);
});
pdfDoc.on('end', () => {
result = Buffer.concat(chunks);
resolve(result);
});
pdfDoc.end();
});
};
What could be wrong here that is preventing the promise and thereby the quotation code to be executed as intended?
On firebase log, all I see is Function execution took 3288 ms, finished with status: 'ok'
Based on the execution time and lack of errors, it looks like you're successfully creating the buffer for the PDF but you're not actually returning it from the function.
.then((profile) => {
return quotPdfHelper.generatePDF(fonts, profile, quotationData, storage);
})
.then(() => {
console.log('Generation Successful. Pass for email');
})
In the code above, you're passing the result to the next then block, but then returning undefined from that block. The end result of this Promise chain will be undefined. To pass the result through, you'd want to return it at the end of the Promise chain:
.then((profile) => {
return quotPdfHelper.generatePDF(fonts, profile, quotationData, storage);
})
.then(buffer => {
console.log('Generation Successful. Pass for email');
return buffer;
})
I'm trying to experiment generating pdf using firebase cloud function but I am blocked about defining fonts parameter.
Here's my definition:
var fonts = {
Roboto: {
normal: './fonts/Roboto-Regular.ttf',
bold: './fonts/Roboto-Bold.ttf',
italics: './fonts/Roboto-Italic.ttf',
bolditalics: './fonts/Roboto-BoldItalic.ttf'
}
};
I've created a fonts folder which contain the for above files. However wherever I set the fonts folder (in root, in functions folder or in node_modules folder), I get the error 'no such file or directory' when deploying functions. Any advice would be very much appreciated.

How to handle more than one window in puppeteer?

I am using puppeteer to do tests with the browser, i've managed to do is to access a page, then i do click in a DOM element, after do click, the browser show me other view, in this view i do click in a button that open a pop up for do log in with facebook.
My question is:
how i can handler the other window for do login with
facebook? this is code.
Example code:
import * as puppeteer from 'puppeteer';
const f = async () => {
console.log('..');
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
page.setViewport({ width: 1200, height: 800 })
await page.goto('https://goalgoodies.herokuapp.com').catch(err => { console.log('error ', err); });
await page.screenshot({ path: 'screenshot.png' });
const resp = await page.click('a').catch(err => { console.log('error click', err); });
const inputElement = await page.$('.signin a').catch(err => { console.log('error selector', err); });
await inputElement.click().catch(err => { console.log('error click', err); });
await page.screenshot({ path: 'screenshot2.png' });
const fbBtn = await page.$('button[name=facebook]');
await fbBtn.click();
// here it's open pop up for do login with facebook
await page.screenshot({ path: 'clickpopup.png' });
};
f();
Apparently there is no way with puppeter to interact with other windows
Here another related question
In this post forum the user aslushnikov mentions something related with Target domain, but I can not understand what he means, or how to execute.
Any help would be appreciated.
Thank you
I think you are looking for Browser Contexts.
https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createBrowserContext
A sample implementation is discussed in details here,
https://github.com/cyrus-and/chrome-remote-interface/issues/118
Hope it helps.
To allow changing between open pages I created a simple utility method:
async function changePage(url) {
let pages = await browser.pages();
let foundPage;
for(let i = 0; i < pages.length; i += 1) {
if(pages[i].url() === url) {
foundPage = pages[i];//return the new working page
break;
}
}
return foundPage;
}
This assumes your timing is correct for any newly opened windows, but that would be a different topic.

Use headless chrome to intercept image request data

I have a use case that needs to use Headless Chrome Network (https://chromedevtools.github.io/devtools-protocol/tot/Network/) to intercept all images requests and find out the image size before saving it (basically discard small images such as icons).
However, I am unable to figure out a way to load the image data in memory before saving it. I need to load it in Img object to get width and height. The Network.getResponseBody is taking requestId which I don't have access in Network.requestIntercepted. Also Network.loadingFinished always gives me "0" in encodedDataLength variable. I have no idea why. So my questions are:
How to intercept all responses from jpg/png request and get the image data? Without saving the file via URL string to the disk and load back.
BEST: how to get image dimension from header response? Then I don't have to read the data into memory at all.
My code is below:
const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
const file = require('fs');
(async function() {
async function launchChrome() {
return await chromeLauncher.launch({
chromeFlags: [
'--disable-gpu',
'--headless'
]
});
}
const chrome = await launchChrome();
const protocol = await CDP({
port: chrome.port
});
const {
DOM,
Network,
Page,
Emulation,
Runtime
} = protocol;
await Promise.all([Network.enable(), Page.enable(), Runtime.enable(), DOM.enable()]);
await Network.setRequestInterceptionEnabled({enabled: true});
Network.requestIntercepted(({interceptionId, request, resourceType}) => {
if ((request.url.indexOf('.jpg') >= 0) || (request.url.indexOf('.png') >= 0)) {
console.log(JSON.stringify(request));
console.log(resourceType);
if (request.url.indexOf("/unspecified.jpg") >= 0) {
console.log("FOUND unspecified.jpg");
console.log(JSON.stringify(interceptionId));
// console.log(JSON.stringify(Network.getResponseBody(interceptionId)));
}
}
Network.continueInterceptedRequest({interceptionId});
});
Network.loadingFinished(({requestId, timestamp, encodedDataLength}) => {
console.log(requestId);
console.log(timestamp);
console.log(encodedDataLength);
});
Page.navigate({
url: 'https://www.yahoo.com/'
});
Page.loadEventFired(async() => {
protocol.close();
chrome.kill();
});
})();
This should get you 90% of the way there. It gets the body of each image request. You'd still need to base64decode, check size and save etc...
const CDP = require('chrome-remote-interface');
const sizeThreshold = 1024;
async function run() {
try {
var client = await CDP();
const { Network, Page } = client;
// enable events
await Promise.all([Network.enable(), Page.enable()]);
// commands
const _url = "https://google.co.za";
let _pics = [];
Network.responseReceived(async ({requestId, response}) => {
let url = response ? response.url : null;
if ((url.indexOf('.jpg') >= 0) || (url.indexOf('.png') >= 0)) {
const {body, base64Encoded} = await Network.getResponseBody({ requestId }); // throws promise error returning null/undefined so can't destructure. Must be different in inspect shell to app?
_pics.push({ url, body, base64Encoded });
console.log(url, body, base64Encoded);
}
});
await Page.navigate({ url: _url });
await sleep(5000);
// TODO: process _pics - base64Encoded, check body.length > sizeThreshold, save etc...
} catch (err) {
if (err.message && err.message === "No inspectable targets") {
console.error("Either chrome isn't running or you already have another app connected to chrome - e.g. `chrome-remote-interface inspect`")
} else {
console.error(err);
}
} finally {
if (client) {
await client.close();
}
}
}
function sleep(miliseconds = 1000) {
if (miliseconds == 0)
return Promise.resolve();
return new Promise(resolve => setTimeout(() => resolve(), miliseconds))
}
run();

Resources