<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ puppeteer - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ puppeteer - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 26 Aug 2026 13:29:54 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/puppeteer/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Scrape Amazon Product Reviews Behind a Login ]]>
                </title>
                <description>
                    <![CDATA[ By Satyam Tripathi Amazon is the most popular e-commerce website for web scrapers, with billions of product pages being scraped every month.  It is also home to a vast database of product reviews, which can be very useful for market research and comp... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-scrape-amazon-product-reviews-behind-a-login/</link>
                <guid isPermaLink="false">66d461744bc8f441cb6df837</guid>
                
                    <category>
                        <![CDATA[ node js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ puppeteer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ web scraping ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 30 Oct 2023 16:46:40 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/10/pexels-pixabay-159751--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Satyam Tripathi</p>
<p>Amazon is the most popular e-commerce website for web scrapers, with billions of product pages being scraped every month. </p>
<p>It is also home to a vast database of product reviews, which can be very useful for market research and competitor monitoring. </p>
<p>You can extract relevant data from the Amazon website and save it in a spreadsheet or JSON format. And you can even automate the process to update the data regularly.</p>
<p>Scraping Amazon product reviews is not always straightforward, especially when a login is required. In this guide, you'll learn how to scrape Amazon product reviews behind a login. You’ll learn the process of logging in, parsing review data, and exporting reviews to CSV.</p>
<p><strong>Important Disclaimer:</strong> This tutorial is for educational purposes only. Scraping data from behind logins on websites may violate their terms and conditions (T&amp;Cs).  It's crucial to always check the T&amp;Cs of any website before scraping data.</p>
<p>Without further ado, let's get started.</p>
<h2 id="heading-prerequisites-and-project-setup">Prerequisites and Project Setup</h2>
<p>We’ll use the Node.js Puppeteer library to scrape Amazon reviews. Make sure Node.js is installed on your system. If it is not, go to the official <a target="_blank" href="https://nodejs.org/en">Node.js website</a> and install it. </p>
<p>After Node.js is installed, install Puppeteer. <a target="_blank" href="https://github.com/puppeteer/puppeteer">Puppeteer</a> is a Node.js library that provides a high-level, user-friendly API for automating tasks and interacting with dynamic web pages. </p>
<p>Now, let's install and configure Puppeteer.</p>
<p>Open a terminal and create a new folder with any name. (In my case, it is _amazon<em>reviews</em>).</p>
<pre><code class="lang-bash">mkdir amazon_reviews
</code></pre>
<p>Change your current directory to the folder created above.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> amazon_reviews
</code></pre>
<p>Cool, you're now in the correct directory. Execute the following command to initialize the <em>package.json</em> file:</p>
<pre><code class="lang-bash">npm init -y
</code></pre>
<p>Finally, install Puppeteer using the following command:</p>
<pre><code class="lang-bash">npm install puppeteer
</code></pre>
<p>This is what the process looks like:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-070530.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now, open the folder in any code editor, and create a new JavaScript file (index.js). Make sure that the hierarchy looks like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-070823.png" alt="Image" width="600" height="400" loading="lazy">
_Hierarchy showing <code>node_modules</code>, <code>index.js</code>, <code>package-lock.json</code>, and <code>package.json</code>_</p>
<p>All set up successfully. We’re now ready to code the scraper.</p>
<p><strong>Note:</strong> Ensure that you have an account on Amazon so you can progress through the rest of this tutorial.</p>
<h2 id="heading-step-1-get-access-to-the-public-page">Step 1: Get Access to the Public Page</h2>
<p>You're going to scrape the reviews of the product shown below. You’ll extract the author's name, review title, and date.</p>
<p>Here's the product URL: <a target="_blank" href="https://www.amazon.com/ENHANCE-Headphone-Customizable-Lighting-Flexible/dp/B07DR59JLP/">https://www.amazon.com/ENHANCE-Headphone-Customizable-Lighting-Flexible/dp/B07DR59JLP/</a></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-072923.png" alt="Image" width="600" height="400" loading="lazy">
<em>The product we're using in the example - headphones</em></p>
<p>First, you’ll log in to Amazon, and then redirect to the product URL to scrape the reviews.</p>
<h2 id="heading-step-2-scrape-behind-the-login">Step 2: Scrape Behind the Login</h2>
<p>Amazon's multi-stage login process requires users to enter their username or email, click a Continue button to enter their password, and then finally submit it. Both the username and password fields are typically on different pages.</p>
<p>To enter the email ID, use the selector <code>input[name=email]</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-082325.png" alt="Image" width="600" height="400" loading="lazy">
<em>HTML of the sign-in field</em></p>
<p>Now, click on the Continue button using the selector <code>input[id=continue]</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-083136.png" alt="Image" width="600" height="400" loading="lazy">
<em>HTML of the continue button</em></p>
<p>Now you should be on the next page. To enter the password, use the selector <code>input[name=password]</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-083415.png" alt="Image" width="600" height="400" loading="lazy">
<em>HTML of the password field</em></p>
<p>Finally, click on the Sign In button using the selector <code>input[id=signInSubmit]</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-083833.png" alt="Image" width="600" height="400" loading="lazy">
<em>HTML of the sign-in button</em></p>
<p>Here’s the code for the login process:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> selectors = {
  <span class="hljs-attr">emailid</span>: <span class="hljs-string">'input[name=email]'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'input[name=password]'</span>,
  <span class="hljs-attr">continue</span>: <span class="hljs-string">'input[id=continue]'</span>,
  <span class="hljs-attr">singin</span>: <span class="hljs-string">'input[id=signInSubmit]'</span>,
};


    <span class="hljs-keyword">await</span> page.goto(signinURL);
    <span class="hljs-keyword">await</span> page.waitForSelector(selectors.emailid);
    <span class="hljs-keyword">await</span> page.type(selectors.emailid, <span class="hljs-string">"satyam@gmail.com"</span>, { <span class="hljs-attr">delay</span>: <span class="hljs-number">100</span> });
    <span class="hljs-keyword">await</span> page.click(selectors.continue);
    <span class="hljs-keyword">await</span> page.waitForSelector(selectors.password);
    <span class="hljs-keyword">await</span> page.type(selectors.password, <span class="hljs-string">"mypassword"</span>, { <span class="hljs-attr">delay</span>: <span class="hljs-number">100</span> });
    <span class="hljs-keyword">await</span> page.click(selectors.singin);
    <span class="hljs-keyword">await</span> page.waitForNavigation();
</code></pre>
<p>We're following the same steps as discussed above. First, go to the sign-in URL, enter the email ID, and click on the Continue button. Then enter the password, click on the Sign In button, and wait for a moment for the sign-in process to complete.</p>
<p>After the sign-in process is completed, you’ll be redirected to the product page to scrape the reviews.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-072923-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>Product page</em></p>
<h2 id="heading-step-3-parse-the-review-data">Step 3: Parse the Review Data</h2>
<p>You've successfully logged in and are now on the product page that you want to scrape. Let's now parse the review data.</p>
<p>On the page, you'll find various reviews. These reviews are contained within a parent <code>div</code> with the ID <code>cm-cr-dp-review-list</code>, which holds all the reviews on the current page. If you want to access more reviews, you'll need to navigate to the next page using the pagination process.</p>
<p>This parent div has multiple child divs, and each child div holds one review. To extract the reviews, you can use the selector <code>#cm-cr-dp-review-list div.review</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> selectors = {
  <span class="hljs-attr">allReviews</span>: <span class="hljs-string">'#cm-cr-dp-review-list div.review'</span>,
  <span class="hljs-attr">authorName</span>: <span class="hljs-string">'div[data-hook="genome-widget"] span.a-profile-name'</span>,
  <span class="hljs-attr">reviewTitle</span>: <span class="hljs-string">'[data-hook=review-title]&gt;span:not([class])'</span>,
  <span class="hljs-attr">reviewDate</span>: <span class="hljs-string">'span[data-hook=review-date]'</span>,
};
</code></pre>
<p>This selector shows that you first go to the element with the ID <code>cm-cr-dp-review-list</code>, then search for all <code>div</code> elements with the data-hook <code>review</code>. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/annotely_image.png" alt="Image" width="600" height="400" loading="lazy">
<em>Review data with Author name, Review Title, Description, etc.</em></p>
<p>The following code snippet shows that you should first go to the product URL, wait for the selector to load, and then scrape all the reviews and store them in the <code>reviewElements</code> variable.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> page.goto(productURL);
<span class="hljs-keyword">await</span> page.waitForSelector(selectors.allReviews);
<span class="hljs-keyword">const</span> reviewElements = <span class="hljs-keyword">await</span> page.$$(selectors.allReviews);
</code></pre>
<p>Now, let's extract the author's name, review title, and date.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-091701.png" alt="Image" width="600" height="400" loading="lazy">
<em>Targetting Author name, Review Title, and Date</em></p>
<p>To parse the author name, you can use the selector <code>div[data-hook="genome-widget"] span.a-profile-name</code>. This selector tells us to first search for the <code>div</code> element with the <code>data-hook</code> attribute set to <code>genome-widget</code>, because the names are inside this <code>div</code> element. Then, search for the <code>span</code> element with the class name <code>a-profile-name</code>. This is the element that contains the author's name.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> author = <span class="hljs-keyword">await</span> reviewElement.$eval(selectors.authorName, <span class="hljs-function">(<span class="hljs-params">element</span>) =&gt;</span> element.textContent);
</code></pre>
<p>To parse the review title, you can use the CSS selector <code>[data-hook="review-title"] &gt; span:not([class])</code>. This selector tells us to search for the <code>span</code> element that is a direct child of the <code>[data-hook="review-title"]</code> element and that does not have a class attribute.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> title = <span class="hljs-keyword">await</span> reviewElement.$eval(selectors.reviewTitle, <span class="hljs-function">(<span class="hljs-params">element</span>) =&gt;</span> element.textContent);
</code></pre>
<p>To parse the date, you can use the CSS selector <code>span[data-hook="review-date"]</code>. This selector tells us to search for the span element that has the <code>data-hook</code> attribute set to <code>review-date</code>. This is the element that contains the review date.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> rawReviewDate = <span class="hljs-keyword">await</span> reviewElement.$eval(selectors.reviewDate, <span class="hljs-function">(<span class="hljs-params">element</span>) =&gt;</span> element.textContent);
</code></pre>
<p>Note that you’ll get the entire text, including the location, instead of just the full date. Therefore, you must use a regular expression pattern to extract the date from the text. </p>
<p>After that, combine all of the data into the <code>reviewData</code> and then push it to the final list <code>reviewsData</code>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> datePattern = <span class="hljs-regexp">/(\w+\s\d{1,2},\s\d{4})/</span>;
      <span class="hljs-keyword">const</span> match = rawReviewDate.match(datePattern);
      <span class="hljs-keyword">const</span> reviewDate = match ? match[<span class="hljs-number">0</span>].replace(<span class="hljs-string">','</span>, <span class="hljs-string">''</span>) : <span class="hljs-string">"Date not found"</span>;

      <span class="hljs-keyword">const</span> reviewData = {
        author,
        title,
        reviewDate,
      };

      reviewsData.push(reviewData);
    }
</code></pre>
<p>The above process will run until it has parsed all of the reviews on the current page. Here’s the code snippet to parse the data:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> reviewElement <span class="hljs-keyword">of</span> reviewElements) {
      <span class="hljs-keyword">const</span> author = <span class="hljs-keyword">await</span> reviewElement.$eval(selectors.authorName, <span class="hljs-function">(<span class="hljs-params">element</span>) =&gt;</span> element.textContent);
      <span class="hljs-keyword">const</span> title = <span class="hljs-keyword">await</span> reviewElement.$eval(selectors.reviewTitle, <span class="hljs-function">(<span class="hljs-params">element</span>) =&gt;</span> element.textContent);
      <span class="hljs-keyword">const</span> rawReviewDate = <span class="hljs-keyword">await</span> reviewElement.$eval(selectors.reviewDate, <span class="hljs-function">(<span class="hljs-params">element</span>) =&gt;</span> element.textContent);

      <span class="hljs-keyword">const</span> datePattern = <span class="hljs-regexp">/(\w+\s\d{1,2},\s\d{4})/</span>;
      <span class="hljs-keyword">const</span> match = rawReviewDate.match(datePattern);
      <span class="hljs-keyword">const</span> reviewDate = match ? match[<span class="hljs-number">0</span>].replace(<span class="hljs-string">','</span>, <span class="hljs-string">''</span>) : <span class="hljs-string">"Date not found"</span>;

      <span class="hljs-keyword">const</span> reviewData = {
        author,
        title,
        reviewDate,
      };

      reviewsData.push(reviewData);
    }
</code></pre>
<p>Great! You’ve successfully parsed the relevant data, which is now in JSON format, as shown below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-095917.png" alt="Image" width="600" height="400" loading="lazy">
<em>Scraped the data in JSON format</em></p>
<h2 id="heading-step-4-export-reviews-to-a-csv">Step 4: Export Reviews to a CSV</h2>
<p>You've parsed the reviews in JSON format, which is a bit human-readable. You can convert this data to CSV format to make it more readable and easier for other purposes. </p>
<p>There are many ways to convert JSON data to CSV, but we'll use a simple and effective approach. Here is a simple code snippet to convert JSON to CSV:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> csvContent = <span class="hljs-string">"Author,Title,Date\n
for (const review of reviewsData) {
      const { author, title, reviewDate } = review;
      csvContent += `${author},"</span>${title}<span class="hljs-string">",${reviewDate}\n`;
    }

const csvFileName = "</span>amazon_reviews.csv<span class="hljs-string">";
await fs.writeFileSync(csvFileName, csvContent, "</span>utf8<span class="hljs-string">");</span>
</code></pre>
<p>Here’s the output of the CSV file.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-27-102705.png" alt="Image" width="600" height="400" loading="lazy">
<em>Converted JSON data into CSV format</em></p>
<p>And there you have it!</p>
<p>You can find the full Code uploaded on GitHub <a target="_blank" href="https://gist.github.com/triposat/20706d61989a4031669c2e3d25f487d0">here</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this guide, you learned how to scrape Amazon product reviews behind a login using Puppeteer. You learned how to log in, parse relevant data, and save it to a CSV file. </p>
<p>To practice more, you can extract all the reviews of all the pages using pagination.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Web Scraping in JavaScript – How to Use Puppeteer to Scrape Web Pages ]]>
                </title>
                <description>
                    <![CDATA[ Welcome to the world of web scraping! Have you ever needed data from a website but found it hard to access it in a structured format? This is where web scraping comes in. Using scripts, we can extract the data we need from a website for various purpo... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/web-scraping-in-javascript-with-puppeteer/</link>
                <guid isPermaLink="false">66bb921e0eaca026d8cfa5ed</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ puppeteer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ web scraping ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Gaël Thomas ]]>
                </dc:creator>
                <pubDate>Tue, 31 Jan 2023 15:26:55 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/01/web-scraping-in-javascript-with-puppeteer.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Welcome to the world of web scraping! Have you ever needed data from a website but found it hard to access it in a structured format? This is where web scraping comes in.</p>
<p>Using scripts, we can extract the data we need from a website for various purposes, such as creating databases, doing some analytics, and even more.</p>
<blockquote>
<p><strong>Disclaimer:</strong> Be careful when doing web scraping. Always make sure you're scraping sites that allow it, and performing this activity within ethical and legal limits.</p>
</blockquote>
<p>JavaScript and Node.js offers various libraries that make web scraping easier. For simple data extraction, you can use Axios to fetch an API responses or a website HTML. </p>
<p>But if you're looking to do more advanced tasks including automations, you'll need libraries such as <a target="_blank" href="https://pptr.dev/">Puppeteer</a>, <a target="_blank" href="https://cheerio.js.org/">Cheerio</a>, or <a target="_blank" href="https://github.com/segmentio/nightmare">Nightmare</a> (don't worry the name is nightmare, but it's not that bad to use 😆).</p>
<p>I'll introduce the basics of web scraping in JavaScript and Node.js using Puppeteer in this article. I structured the writing to show you some basics of fetching information on a website and clicking a button (for example, moving to the next page).</p>
<p>At the end of this introduction, I'll recommend ways to practice and learn more by improving the project we just created.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving in and scraping our first page together using JavaScript, Node.js, and the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction">HTML DOM</a>, I'd recommend having a basic understanding of these technologies. It'll improve your learning and understanding of the topic.</p>
<p>Let's dive in! 🤿</p>
<h2 id="heading-how-to-initialize-your-first-puppeteer-scraper">How to Initialize Your First Puppeteer Scraper</h2>
<p>New project...new folder! First, create the <code>first-puppeteer-scraper-example</code> folder on your computer. It'll contain the code of our future scraper.</p>
<pre><code class="lang-shell">mkdir first-puppeteer-scraper-example
</code></pre>
<p>Now, it's time to initialize your Node.js repository with a package.json file. It's helpful to add information to the repository and NPM packages, such as the Puppeteer library.</p>
<pre><code class="lang-shell">npm init -y
</code></pre>
<p>After typing this command, you should find this <code>package.json</code> file in your repository tree.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"first-puppeteer-scraper-example"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"keywords"</span>: [],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"puppeteer"</span>: <span class="hljs-string">"^19.6.2"</span>
  },
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {},
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p>Before proceeding, we must ensure the project is configured to handle ES6 features. To do so, you can add the <code>"types": "module"</code> instruction at the end of the configuration.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"first-puppeteer-scraper-example"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"keywords"</span>: [],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"puppeteer"</span>: <span class="hljs-string">"^19.6.2"</span>
  },
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"types"</span>: <span class="hljs-string">"module"</span>
}
</code></pre>
<p>The last step of our scraper initialization is to install the Puppeteer library. Here's how:</p>
<pre><code class="lang-shell">npm install puppeteer
</code></pre>
<p>Wow! We're there – we're ready to scrape our first website together. 🤩</p>
<h2 id="heading-how-to-scrape-your-first-piece-of-data">How to Scrape Your First Piece of Data</h2>
<p>In this article, we'll use the <a target="_blank" href="https://toscrape.com/">ToScrape</a> website as our learning platform. This online sandbox provides two projects specifically designed for web scraping, making it a great starting point to learn the basics such as data extraction and page navigation.</p>
<p>For this beginner's introduction, we'll specifically focus on the <a target="_blank" href="http://quotes.toscrape.com/">Quotes to Scrape</a> website.</p>
<h3 id="heading-how-to-initialize-the-script">How to Initialize the Script</h3>
<p>In the project repository root, you can create an <code>index.js</code> file. This will be our application entry point.</p>
<p>To keep it simple, our script consists of one function in charge of getting the website's quotes (<code>getQuotes</code>).</p>
<p>In the function's body, we will need to follow different steps:</p>
<ul>
<li>Start a Puppeteer session with <code>puppeteer.launch</code> (it'll instantiate a <code>browser</code> variable that we'll use for manipulating the browser)</li>
<li>Open a new page/tab with <code>browser.newPage</code> (it'll instantiate a <code>page</code> variable that we'll use for manipulating the page)</li>
<li>Change the URL of our new page to <a target="_blank" href="http://quotes.toscrape.com/"><code>http://quotes.toscrape.com/</code></a> with <code>page.goto</code></li>
</ul>
<p>Here's the commented version of the initial script:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> puppeteer <span class="hljs-keyword">from</span> <span class="hljs-string">"puppeteer"</span>;

<span class="hljs-keyword">const</span> getQuotes = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-comment">// Start a Puppeteer session with:</span>
  <span class="hljs-comment">// - a visible browser (`headless: false` - easier to debug because you'll see the browser in action)</span>
  <span class="hljs-comment">// - no default viewport (`defaultViewport: null` - website page will in full width and height)</span>
  <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch({
    <span class="hljs-attr">headless</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">defaultViewport</span>: <span class="hljs-literal">null</span>,
  });

  <span class="hljs-comment">// Open a new page</span>
  <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();

  <span class="hljs-comment">// On this new page:</span>
  <span class="hljs-comment">// - open the "http://quotes.toscrape.com/" website</span>
  <span class="hljs-comment">// - wait until the dom content is loaded (HTML is ready)</span>
  <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">"http://quotes.toscrape.com/"</span>, {
    <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">"domcontentloaded"</span>,
  });
};

<span class="hljs-comment">// Start the scraping</span>
getQuotes();
</code></pre>
<p>What do you think of running our scraper and seeing the output? Let's do it with the command below:</p>
<pre><code class="lang-shell">node index.js
</code></pre>
<p>After doing this, you should have a brand new browser application started with a new page and the website Quotes to Scrape loaded onto it. Magic, isn't it? 🪄</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-353.png" alt="Image" width="600" height="400" loading="lazy">
<em>Quotes to Scrape homepage loaded by our initial script</em></p>
<p><strong>Note:</strong> For this first iteration, we're not closing the browser. This means you will need to close the browser to stop the running application.</p>
<h3 id="heading-how-to-fetch-the-first-quote">How to Fetch the First Quote</h3>
<p>Whenever you want to scrape a website, you'll have to play with the HTML DOM. What I recommend is to inspect the page and start navigating the different elements to find what you need.</p>
<p>In our case, we'll follow the <a target="_blank" href="https://dictionary.cambridge.org/dictionary/english/baby-step">baby step principle</a> and start fetching the first quote, author, and text.</p>
<p>After browsing the page HTML, we can notice a quote is encapsulated in a <code>&lt;div&gt;</code> element with a class name <code>quote</code> (<code>class="quote"</code>). This is important information because the scraping works with <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors">CSS selectors</a> (for example, .quote).</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-354.png" alt="Image" width="600" height="400" loading="lazy">
<em>Browser inspector with the first quote <code>&amp;lt;div&amp;gt;</code> selected</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-355.png" alt="Image" width="600" height="400" loading="lazy">
<em>An example of how each quote is rendered in the HTML</em></p>
<p>Now that we have this knowledge, we can return to our <code>getQuotes</code> function and improve our code to select the first quote and extract its data.</p>
<p>We will need to add the following after the <code>page.goto</code> instruction:</p>
<ul>
<li>Extract data from our page HTML with <code>page.evaluate</code> (it'll execute the function passed as a parameter in the page context and returns the result)</li>
<li>Get the quote HTML node with <code>document.querySelector</code> (it'll fetch the first <code>&lt;div&gt;</code> with the classname <code>quote</code> and returns it)</li>
<li>Get the quote text and author from the previously extracted quote HTML node with <code>quote.querySelector</code> (it'll extract the elements with the classname <code>text</code> and <code>author</code> under <code>&lt;div class="quote"&gt;</code> and returns them)</li>
</ul>
<p>Here's the updated version with detailed comments:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> puppeteer <span class="hljs-keyword">from</span> <span class="hljs-string">"puppeteer"</span>;

<span class="hljs-keyword">const</span> getQuotes = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-comment">// Start a Puppeteer session with:</span>
  <span class="hljs-comment">// - a visible browser (`headless: false` - easier to debug because you'll see the browser in action)</span>
  <span class="hljs-comment">// - no default viewport (`defaultViewport: null` - website page will in full width and height)</span>
  <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch({
    <span class="hljs-attr">headless</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">defaultViewport</span>: <span class="hljs-literal">null</span>,
  });

  <span class="hljs-comment">// Open a new page</span>
  <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();

  <span class="hljs-comment">// On this new page:</span>
  <span class="hljs-comment">// - open the "http://quotes.toscrape.com/" website</span>
  <span class="hljs-comment">// - wait until the dom content is loaded (HTML is ready)</span>
  <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">"http://quotes.toscrape.com/"</span>, {
    <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">"domcontentloaded"</span>,
  });

  <span class="hljs-comment">// Get page data</span>
  <span class="hljs-keyword">const</span> quotes = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// Fetch the first element with class "quote"</span>
    <span class="hljs-keyword">const</span> quote = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".quote"</span>);

    <span class="hljs-comment">// Fetch the sub-elements from the previously fetched quote element</span>
    <span class="hljs-comment">// Get the displayed text and return it (`.innerText`)</span>
    <span class="hljs-keyword">const</span> text = quote.querySelector(<span class="hljs-string">".text"</span>).innerText;
    <span class="hljs-keyword">const</span> author = quote.querySelector(<span class="hljs-string">".author"</span>).innerText;

    <span class="hljs-keyword">return</span> { text, author };
  });

  <span class="hljs-comment">// Display the quotes</span>
  <span class="hljs-built_in">console</span>.log(quotes);

  <span class="hljs-comment">// Close the browser</span>
  <span class="hljs-keyword">await</span> browser.close();
};

<span class="hljs-comment">// Start the scraping</span>
getQuotes();
</code></pre>
<p>Something interesting to point out is that the function name for selecting an element is the same as in the browser inspect. Here's an example:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-362.png" alt="Image" width="600" height="400" loading="lazy">
<em>After running the <code>document.querySelector</code> instruction in the browser inspector, we have the first quote as an output (like on Puppeteer)</em></p>
<p>Let's run our script one more time and see what we have as an output:</p>
<pre><code class="lang-json">{
  text: '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
  author: 'Albert Einstein'
}
</code></pre>
<p>We did it! Our first scraped element is here, right in the terminal. Now, let's expand it and fetch all the current page quotes. 🔥</p>
<h3 id="heading-how-to-fetch-all-current-page-quotes">How to Fetch All Current Page Quotes</h3>
<p>Now that we know how to fetch one quote, let's trick our code a bit to get all the quotes and extract their data one by one.</p>
<p>Previously we used <code>document.getQuerySelector</code> to select the first matching element (the first quote). To be able to fetch all quotes, we will need the <code>document.querySelectorAll</code> function instead.</p>
<p>We'll need to follow these steps to make it work:</p>
<ul>
<li>Replace <code>document.getQuerySelector</code> with <code>document.querySelectorAll</code> (it'll fetch all <code>&lt;div&gt;</code> elements with the classname <code>quote</code> and return them)</li>
<li>Convert the fetched elements to a list with <code>Array.from(quoteList)</code> (it'll ensure the list of quotes is iterable)</li>
<li>Move our previous code to get the quote text and author inside the loop and return the result (it'll extract the elements with the classname <code>text</code> and <code>author</code> under <code>&lt;div class="quote"&gt;</code> for each quote)</li>
</ul>
<p>Here's the code update:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> puppeteer <span class="hljs-keyword">from</span> <span class="hljs-string">"puppeteer"</span>;

<span class="hljs-keyword">const</span> getQuotes = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-comment">// Start a Puppeteer session with:</span>
  <span class="hljs-comment">// - a visible browser (`headless: false` - easier to debug because you'll see the browser in action)</span>
  <span class="hljs-comment">// - no default viewport (`defaultViewport: null` - website page will be in full width and height)</span>
  <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch({
    <span class="hljs-attr">headless</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">defaultViewport</span>: <span class="hljs-literal">null</span>,
  });

  <span class="hljs-comment">// Open a new page</span>
  <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();

  <span class="hljs-comment">// On this new page:</span>
  <span class="hljs-comment">// - open the "http://quotes.toscrape.com/" website</span>
  <span class="hljs-comment">// - wait until the dom content is loaded (HTML is ready)</span>
  <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">"http://quotes.toscrape.com/"</span>, {
    <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">"domcontentloaded"</span>,
  });

  <span class="hljs-comment">// Get page data</span>
  <span class="hljs-keyword">const</span> quotes = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// Fetch the first element with class "quote"</span>
    <span class="hljs-comment">// Get the displayed text and returns it</span>
    <span class="hljs-keyword">const</span> quoteList = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">".quote"</span>);

    <span class="hljs-comment">// Convert the quoteList to an iterable array</span>
    <span class="hljs-comment">// For each quote fetch the text and author</span>
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>.from(quoteList).map(<span class="hljs-function">(<span class="hljs-params">quote</span>) =&gt;</span> {
      <span class="hljs-comment">// Fetch the sub-elements from the previously fetched quote element</span>
      <span class="hljs-comment">// Get the displayed text and return it (`.innerText`)</span>
      <span class="hljs-keyword">const</span> text = quote.querySelector(<span class="hljs-string">".text"</span>).innerText;
      <span class="hljs-keyword">const</span> author = quote.querySelector(<span class="hljs-string">".author"</span>).innerText;

      <span class="hljs-keyword">return</span> { text, author };
    });
  });

  <span class="hljs-comment">// Display the quotes</span>
  <span class="hljs-built_in">console</span>.log(quotes);

  <span class="hljs-comment">// Close the browser</span>
  <span class="hljs-keyword">await</span> browser.close();
};

<span class="hljs-comment">// Start the scraping</span>
getQuotes();
</code></pre>
<p>As an end result, if we run our script one more time, we should have a list of quotes as an output. Each element of this list should have a text and an author property.</p>
<pre><code class="lang-json">[
  {
    text: '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
    author: 'Albert Einstein'
  },
  {
    text: '“It is our choices, Harry, that show what we truly are, far more than our abilities.”',
    author: 'J.K. Rowling'
  },
  {
    text: '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”',
    author: 'Albert Einstein'
  },
  {
    text: '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”',
    author: 'Jane Austen'
  },
  {
    text: <span class="hljs-string">"“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”"</span>,
    author: 'Marilyn Monroe'
  },
  {
    text: '“Try not to become a man of success. Rather become a man of value.”',
    author: 'Albert Einstein'
  },
  {
    text: '“It is better to be hated for what you are than to be loved for what you are not.”',
    author: 'André Gide'
  },
  {
    text: <span class="hljs-string">"“I have not failed. I've just found 10,000 ways that won't work.”"</span>,
    author: 'Thomas A. Edison'
  },
  {
    text: <span class="hljs-string">"“A woman is like a tea bag; you never know how strong it is until it's in hot water.”"</span>,
    author: 'Eleanor Roosevelt'
  },
  {
    text: '“A day without sunshine is like, you know, night.”',
    author: 'Steve Martin'
  }
]
</code></pre>
<p>Good job! All the quotes from the first page are now scraped by our script. 👏</p>
<h3 id="heading-how-to-move-to-the-next-page">How to Move to the Next Page</h3>
<p>Our script is now able to fetch all the quotes for one page. What would be interesting is clicking on the "Next page" at the page bottom and doing the same on the second page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-363.png" alt="Image" width="600" height="400" loading="lazy">
<em>"Next" button at the Quotes to Scrape page bottom</em></p>
<p>Back to our browser inspect, and let's find how we can target this element using CSS selectors. </p>
<p>As we can notice, the next button is placed under an unordered list <code>&lt;ul&gt;</code> with a <code>pager</code> classname (<code>&lt;ul class="pager"&gt;</code>). This list has an element <code>&lt;li&gt;</code> with a <code>next</code> classname (<code>&lt;li class="next"&gt;</code>). Finally, there is a link anchor <code>&lt;a&gt;</code> that links to the second page (<code>&lt;a href="/page/2/"&gt;</code>).</p>
<p>In CSS, if we want to target this specific link there are different ways to do that. We can do:</p>
<ul>
<li><code>.next &gt; a</code>: but, it's risky because if there is an other element with <code>.next</code> as a parent element containing a link, it'll click on it.</li>
<li><code>.pager &gt; .next &gt; a</code>: safer, because we make sure the link should be inside the <code>.pager</code> parent element under the <code>.next</code> element. There is a low risk of having this hierarchy more than once.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-356.png" alt="Image" width="600" height="400" loading="lazy">
<em>An example of how the "Next" button is rendered in the HTML</em></p>
<p>To click this button, at the end of our script after the <code>console.log(quotes);</code>, you can add the following: <code>await page.click(".pager &gt; .next &gt; a");</code>.</p>
<p>Since we're now closing the browser page with <code>await browser.close();</code> after all instructions are done, you need to comment on this instruction to see the second page opened in the scraper browser.</p>
<p>It's temporary and for testing purposes, but the end of our <code>getQuotes</code> function should look like this:</p>
<pre><code class="lang-javascript">  <span class="hljs-comment">// Display the quotes</span>
  <span class="hljs-built_in">console</span>.log(quotes);

  <span class="hljs-comment">// Click on the "Next page" button</span>
  <span class="hljs-keyword">await</span> page.click(<span class="hljs-string">".pager &gt; .next &gt; a"</span>);

  <span class="hljs-comment">// Close the browser</span>
  <span class="hljs-comment">// await browser.close();</span>
</code></pre>
<p>After this, if you run our scraper again, after processing all instructions, your browser should stop on the second page:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/image-357.png" alt="Image" width="600" height="400" loading="lazy">
<em>Quotes to Scrape second page loaded after clicking the "Next" button</em></p>
<h2 id="heading-its-your-time-heres-what-you-can-do-next">It’s Your Time! Here’s What You Can Do Next:</h2>
<p>Congrats on reaching the end of this introduction to scraping with Puppeteer! 👏</p>
<p>Now it's your turn to improve the scraper and make it get more data from the Quotes to Scrape website. Here's a list of potential improvements you can make:</p>
<ul>
<li>Navigate between all pages using the "Next" button and fetch the quotes on all the pages.</li>
<li>Fetch the quote's tags (each quote has a list of tags).</li>
<li>Scrape the author's about page (by clicking on the author's name on each quote).</li>
<li>Categorize the quotes by tags or authors (it's not 100% related to the scraping itself, but that can be a good improvement).</li>
</ul>
<p>Feel free to be creative and do any other things you see fit 🚀</p>
<h3 id="heading-scraper-code-is-available-on-github">Scraper Code Is Available on GitHub</h3>
<p>Check out the latest version of our scraper on GitHub! You're free to save, fork, or utilize it as you see fit.</p>
<p>=&gt; <a target="_blank" href="https://github.com/gaelgthomas/first-puppeteer-scraper-example">First Puppeteer Scraper (example)</a></p>
<h2 id="heading-successful-scraping-start-thanks-for-reading-the-article">Successful Scraping Start: Thanks for reading the article!</h2>
<p>I hope this article gave you a valuable introduction to web scraping using JavaScript and Puppeteer. Writing this was a pleasure, and I hope you found it informative and enjoyable.</p>
<p><a target="_blank" href="https://twitter.com/gaelgthomas">Join me on Twitter</a> for more content like this. I regularly share content to help you grow your web development skills and would love to have you join the conversation. Let's learn, grow, and inspire each other along the way!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Puppeteer With Node.js ]]>
                </title>
                <description>
                    <![CDATA[ Puppeteer is a JavaScript library that allows you to script and interact with browser windows. In this guide, we'll explore the basics of using Puppeteer with Node.js so you can start automating your tests. Prerequisites Basic understanding of Node.... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-puppeteer-with-nodejs/</link>
                <guid isPermaLink="false">66ba6122e5ad5bacb410af28</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ node ]]>
                    </category>
                
                    <category>
                        <![CDATA[ puppeteer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ valentine Gatwiri ]]>
                </dc:creator>
                <pubDate>Mon, 18 Jul 2022 16:41:54 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/07/pup.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Puppeteer is a JavaScript library that allows you to script and interact with browser windows.</p>
<p>In this guide, we'll explore the basics of using Puppeteer with Node.js so you can start automating your tests.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li>Basic understanding of Node.js</li>
<li>Basic understanding of Puppeteer</li>
<li>A suitable IDE such as VS Code</li>
</ul>
<h3 id="heading-what-youll-learn">What You'll Learn</h3>
<ul>
<li>What is puppeteer?</li>
<li>What is Node.js?</li>
<li>How to set up your first test with Puppeteer</li>
<li>How to run headless Chrome tests on a CI server</li>
</ul>
<h2 id="heading-what-is-puppeteer"><strong>What is Puppeteer?</strong></h2>
<p>Puppeteer is a Node.js library developed by Google that lets you control headless Chrome through the DevTools Protocol.</p>
<p>It is a tool for automating testing in your application using headless Chrome or Chromebit devices, without requiring any browser extensions like Selenium Webdriver or PhantomJS.</p>
<p>Puppeteer lets you automate the testing of your web applications. With it, you can run tests in the browser and then see the results in real-time on your terminal.</p>
<p>Puppeteer uses the WebDriver protocol to connect with the browser and simulate user interaction with HTML elements or pages.</p>
<h2 id="heading-what-is-nodejs"><strong>What is Node.js?</strong></h2>
<p>Node.js is an open-source JavaScript runtime built on Chrome's V8 engine that runs on Linux, Mac OS X, and Windows operating systems. It was first released in 2009 by Ryan Dahl who was one of its original contributors (with some help from Douglas Crockford).</p>
<p>Node.js has become immensely popular over the years as an essential part of many software development projects. It has extensive capabilities when it comes down to coding certain tasks like server-side applications or peer-to-peer networking protocols like Websockets.</p>
<h3 id="heading-how-to-set-up-nodejs-and-puppeteer">How to Set Up Node.js and Puppeteer</h3>
<p>First make a directory which you will be working with by right clicking on your preferred location and choosing new folder. You can also use the command <code>mkdir dir-name</code> in your terminal.</p>
<p>Then create an <code>app.js</code> file in your folder and add the <code>node.js</code> code as shown below:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(<span class="hljs-string">'puppeteer'</span>);

(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch();
    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();
    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">'https://www.freecodecamp.org/'</span>);

    <span class="hljs-keyword">await</span> browser.close();
})();
</code></pre>
<p>The code above creates an instance of the browser which lets Puppeteer launch. Let's make sure we understand the code above:</p>
<ul>
<li><code>browser.newPage()</code> creates new page</li>
<li><code>page.goto()</code> provides the URL to <code>browser.newPage()</code></li>
<li><code>browser.close()</code> closes the running process</li>
</ul>
<p>Now open your terminal and <code>cd</code> into the folder. Then run <code>npm init</code> to create a <code>package.json</code> file.</p>
<p>Press enter then type yes if asked 'is this ok'.</p>
<p>Your output will look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/image-182.png" alt="Image" width="600" height="400" loading="lazy">
<em>package.json</em></p>
<p>Follow the setup instructions to install the dependencies that we will use in our project.</p>
<h2 id="heading-how-to-set-up-your-first-test-with-puppeteer"><strong>How to Set Up Your First Test with Puppeteer</strong></h2>
<p>To use Puppeteer with Node.js, you'll need to install several packages and set up a few environment variables. This part will walk you through the steps you'll need to follow to use Puppeteer in your tests:</p>
<ul>
<li>Download and install <a target="_blank" href="https://nodejs.org/">Node.js</a></li>
<li>Install <a target="_blank" href="https://www.npmjs.com/package/puppeteer">Puppeteer</a> </li>
<li>Install <a target="_blank" href="https://www.npmjs.com/package/mocha">Mocha</a></li>
<li>Install <a target="_blank" href="https://www.npmjs.com/package/chai">Chai</a></li>
<li>Install <a target="_blank" href="https://www.npmjs.com/package/selenium-webdriver">Selenium Webdriver</a></li>
</ul>
<p>You only need to complete the last step if you want to run tests on an actual browser instead of just testing against web driver scripts.</p>
<p>If this is your case, then go ahead and install the selenium-web driver module from the npm package manager by typing <code>npm i selenium-webdriver --save</code>.</p>
<p>Installing the dependencies will generate <code>node_modules</code> and a <code>package-lock.json</code> file as shown below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/image-183.png" alt="Image" width="600" height="400" loading="lazy">
<em>package-lock.json</em></p>
<p>Screenshots are a great way to capture information in your browser. Well, Puppeteer has got you covered!</p>
<p>To take a screenshot of the webpage you navigated to, add the code snippet below:</p>
<pre><code class="lang-js">  <span class="hljs-keyword">await</span> page.screenshot({<span class="hljs-attr">path</span>: <span class="hljs-string">'example.png'</span>});
</code></pre>
<p>To run the application:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> puppeter-tut
<span class="hljs-built_in">cd</span> src
</code></pre>
<p>Then type the command below in your terminal:</p>
<pre><code class="lang-bash">node app.js
</code></pre>
<p>You can also create a PDF by adding the following snippet in your code:</p>
<pre><code class="lang-js">    <span class="hljs-keyword">await</span> page.pdf({ <span class="hljs-attr">path</span>: <span class="hljs-string">'example.pdf'</span> });
</code></pre>
<p>The above code snippet will give us the output shown below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/image-188.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-test-your-setup">How to Test Your Setup</h2>
<p>To test your setup, create a <code>test</code> folder in your code, then add <code>example.test.js</code>.</p>
<p>Your file should contain the following code:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(<span class="hljs-string">'puppeteer'</span>)

describe(<span class="hljs-string">"My first Setup Testing"</span>,<span class="hljs-function">()=&gt;</span>{
     it(<span class="hljs-string">"Home landing page"</span>,<span class="hljs-keyword">async</span>()=&gt;{
    <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch({<span class="hljs-attr">headless</span>:<span class="hljs-literal">false</span>})
     });
});
</code></pre>
<p>Run your test using <code>npm run test</code>. After running your test you will get the following output:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/07/image-187.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Here's a <a target="_blank" href="https://github.com/gatwirival/puppeteer-tut.git">GitHub link to the tutorial's source-code</a>.</p>
<h2 id="heading-how-to-use-one-browser-instance-with-puppeteer">How to Use One Browser Instance with Puppeteer</h2>
<p>As a web developer, you can use Puppeteer to run scripts in the headless Chrome browser and access the window object. This is useful when testing apps that need access to web resources like localStorage or cookies.</p>
<p>To use one browser instance with Puppeteer, you just need to pass <code>{ headless: false }</code> to the launch method. It's asynchronous so it won't block the main thread and make your application unresponsive. </p>
<p>The best thing about this method is that, once it's launched, it should only be used once. Otherwise you will get an error when trying to access any webpage from Puppeteer again.</p>
<p><strong>Here's an example:</strong></p>
<pre><code class="lang-js"><span class="hljs-keyword">let</span> browser; (<span class="hljs-keyword">async</span>() =&gt; { <span class="hljs-keyword">if</span>(!browser) browser = <span class="hljs-keyword">await</span> puppeteer.launch({<span class="hljs-attr">headless</span>: <span class="hljs-literal">false</span>});
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>So, there you have it! Now you know how to get started with Puppeteer and Node.js. </p>
<p>I hope this guide has helped you become more familiar with the tool and its capabilities. Feel free to get in touch with me if you have any questions or suggestions.</p>
<p>‌‌</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Custom API From Any Website Using Puppeteer ]]>
                </title>
                <description>
                    <![CDATA[ By Tarique Ejaz It often happens that you come across a website and are forced to perform a set of actions to finally get some data. You are then faced with a dilemma: how do you make this data available in a form which can easily be consumed by your... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-api-website-using-puppeteer/</link>
                <guid isPermaLink="false">66d4614c246e57ac83a2c7d7</guid>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ node js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ puppeteer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Wed, 27 May 2020 13:33:54 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/05/F4C23721-4609-4B8B-A907-36ACDF146287.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Tarique Ejaz</p>
<p>It often happens that you come across a website and are forced to perform a set of actions to finally get some data. You are then faced with a dilemma: how do you make this data available in a form which can easily be consumed by your application?</p>
<p>Scraping comes to the rescue in such a case. And selecting the right tool for the job is quite important.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/puppeteer-2-3.png" alt="Image" width="600" height="400" loading="lazy">
<em>API is just a way to look at a Website after all (Source: XKCD Comics)</em></p>
<h2 id="heading-puppeteer-not-just-another-scraping-library">Puppeteer: Not Just Another Scraping Library</h2>
<p><a target="_blank" href="https://github.com/puppeteer/puppeteer">Puppeteer</a> is a Node.js library maintained by the Chrome Devtools Team at Google. It basically runs a Chromium or Chrome (perhaps the more recognizable name) instance in a headless (or configurable) manner and exposes a set of high-level APIs. </p>
<p>From its <a target="_blank" href="https://pptr.dev/">official documentation</a>, puppeteer is normally leveraged for multiple processes which are not limited to the following:</p>
<ul>
<li>Generating screenshots and PDFs</li>
<li>Crawling an SPA and generating pre-rendered content (i.e. Server Side Rendering)</li>
<li>Testing Chrome extensions</li>
<li>Automation testing of Web Interfaces</li>
<li>Diagnosis of performance issues through techniques like capturing the timeline trace of a website</li>
</ul>
<p>For our case, we need to be able to access a website and map the data in a form which can be easily consumed by our application. </p>
<p>Sounds simple? The implementation is not that complex, either. Let's start.</p>
<h2 id="heading-stringing-the-code-along">Stringing the Code Along</h2>
<p>My fondness for Amazon products prompts me to use one of their product listing page as a sample here. We will implement our use case in two steps:</p>
<ul>
<li>Extract data from the page and map it in an easily consumable JSON form</li>
<li>Add a little sprinkle of automation to make our lives a little bit easier</li>
</ul>
<p>You can find the complete code in this <a target="_blank" href="https://github.com/tejazz/article-snippets/tree/master/puppeteer-api">repository</a>.</p>
<p>We will be extracting the data from this link: <a target="_blank" href="https://www.amazon.in/s?k=Shirts&amp;ref=nb_sb_noss_2">https://www.amazon.in/s?k=Shirts&amp;ref=nb_sb_noss_2</a> ( a listing of the top searched shirts as shown in the image) in an API servable form.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot--53-.png" alt="Image" width="600" height="400" loading="lazy">
<em>Amazon India - Shirts Listing Page</em></p>
<p>Before we get started using puppeteer extensively in this section, we need to understand the two primary classes provided by it.</p>
<ul>
<li><strong><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&amp;version=v3.1.0&amp;show=api-class-browser">Browser:</a></strong> launches a Chrome instance when we use <code>puppeteer.launch</code> or <code>puppeteer.connect</code> . This works as a simple browser emulation.</li>
<li><strong><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&amp;version=v3.1.0&amp;show=api-class-page">Page:</a></strong> resembles a single tab on a Chrome browser. It provides an exhaustive set of methods you can use with a particular page instance and is invoked when we call <code>browser.newPage</code>. Just like you can create multiple tabs in the browser, you can similarly create multiple page instances at a single time in puppeteer.</li>
</ul>
<h3 id="heading-setting-up-puppeteer-and-navigating-to-the-target-url">Setting Up Puppeteer and Navigating to the Target URL</h3>
<p>We start setting up puppeteer by using the npm module provided. After installing puppeteer, we create an instance of the browser and the page class and navigate to the target URL.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(<span class="hljs-string">'puppeteer'</span>);

<span class="hljs-keyword">const</span> url = <span class="hljs-string">'https://www.amazon.in/s?k=Shirts&amp;ref=nb_sb_noss_2'</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchProductList</span>(<span class="hljs-params">url</span>) </span>{
    <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch({ 
        <span class="hljs-attr">headless</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// false: enables one to view the Chrome instance in action</span>
        <span class="hljs-attr">defaultViewport</span>: <span class="hljs-literal">null</span>, <span class="hljs-comment">// (optional) useful only in non-headless mode</span>
    });
    <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();
    <span class="hljs-keyword">await</span> page.goto(url, { <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">'networkidle2'</span> });
    ...
}

fetchProductList(url);
</code></pre>
<p>We use <code>networkidle2</code> as the value for the <code>waitUntil</code> option while navigating to the URL. This ensures that the page load state is considered final when it has no more than 2 connections running for at least 500ms.</p>
<blockquote>
<p><strong>Note:</strong> You do not need to have Chrome or an instance of it installed on your system for puppeteer to work. It already ships with a lite version of it bundled with the library.</p>
</blockquote>
<h3 id="heading-page-methods-to-extract-and-map-data">Page Methods to Extract and Map Data</h3>
<p>The DOM has already loaded in the page instance created. We will go ahead and leverage the <code>page.evaluate()</code> method to query the DOM. </p>
<p>Before we start, we need to figure out the exact data-points we need to extract. In the current sample, each of the product objects will look something like this.</p>
<pre><code class="lang-js">{
    <span class="hljs-attr">brand</span>: <span class="hljs-string">'Brand Name'</span>, 
    <span class="hljs-attr">product</span>: <span class="hljs-string">'Product Name'</span>,
    <span class="hljs-attr">url</span>: <span class="hljs-string">'https://www.amazon.in/url.of.product.com/'</span>,
    <span class="hljs-attr">image</span>: <span class="hljs-string">'https://www.amazon.in/image.jpg'</span>,
    <span class="hljs-attr">price</span>: <span class="hljs-string">'₹599'</span>,
}
</code></pre>
<p>We have laid out the structure we want to achieve. Time to start inspecting the DOM for the identifiers. We check for the selectors that occur throughout the items to be mapped. We will mostly use <code>[document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)</code> and <code>[document.querySelectorAll](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)</code> for traversing the DOM. </p>
<pre><code class="lang-js">...

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchProductList</span>(<span class="hljs-params">url</span>) </span>{
    ...

    await page.waitFor(<span class="hljs-string">'div[data-cel-widget^="search_result_"]'</span>);

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-comment">// counts total number of products</span>
        <span class="hljs-keyword">let</span> totalSearchResults = <span class="hljs-built_in">Array</span>.from(<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'div[data-cel-widget^="search_result_"]'</span>)).length;

        <span class="hljs-keyword">let</span> productsList = [];

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt; totalSearchResults - <span class="hljs-number">1</span>; i++) {
            <span class="hljs-keyword">let</span> product = {
                <span class="hljs-attr">brand</span>: <span class="hljs-string">''</span>,
                <span class="hljs-attr">product</span>: <span class="hljs-string">''</span>,
            };
            <span class="hljs-keyword">let</span> onlyProduct = <span class="hljs-literal">false</span>;
            <span class="hljs-keyword">let</span> emptyProductMeta = <span class="hljs-literal">false</span>;

            <span class="hljs-comment">// traverse for brand and product names</span>
            <span class="hljs-keyword">let</span> productNodes = <span class="hljs-built_in">Array</span>.from(<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">`div[data-cel-widget="search_result_<span class="hljs-subst">${i}</span>"] .a-size-base-plus.a-color-base`</span>));

            <span class="hljs-keyword">if</span> (productNodes.length === <span class="hljs-number">0</span>) {
                <span class="hljs-comment">// traverse for brand and product names </span>
                <span class="hljs-comment">// (in case previous traversal returned empty elements)</span>
                productNodes = <span class="hljs-built_in">Array</span>.from(<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">`div[data-cel-widget="search_result_<span class="hljs-subst">${i}</span>"] .a-size-medium.a-color-base.a-text-normal`</span>));
                productNodes.length &gt; <span class="hljs-number">0</span> ? onlyProduct = <span class="hljs-literal">true</span> : emptyProductMeta = <span class="hljs-literal">true</span>;
            }

            <span class="hljs-keyword">let</span> productsDetails = productNodes.map(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> el.innerText);

            <span class="hljs-keyword">if</span> (!emptyProductMeta) {
                product.brand = onlyProduct ? <span class="hljs-string">''</span> : productsDetails[<span class="hljs-number">0</span>];
                product.product = onlyProduct ? productsDetails[<span class="hljs-number">0</span>] : productsDetails[<span class="hljs-number">1</span>];
            }

            <span class="hljs-comment">// traverse for product image</span>
            <span class="hljs-keyword">let</span> rawImage = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`div[data-cel-widget="search_result_<span class="hljs-subst">${i}</span>"] .s-image`</span>);
            product.image =rawImage ? rawImage.src : <span class="hljs-string">''</span>;

            <span class="hljs-comment">// traverse for product url</span>
            <span class="hljs-keyword">let</span> rawUrl = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`div[data-cel-widget="search_result_<span class="hljs-subst">${i}</span>"] a[target="_blank"].a-link-normal`</span>);
            product.url = rawUrl ? rawUrl.href : <span class="hljs-string">''</span>;

            <span class="hljs-comment">// traverse for product price</span>
            <span class="hljs-keyword">let</span> rawPrice = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`div[data-cel-widget="search_result_<span class="hljs-subst">${i}</span>"] span.a-offscreen`</span>);
            product.price = rawPrice ? rawPrice.innerText : <span class="hljs-string">''</span>;

            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> product.product !== <span class="hljs-string">'undefined'</span>) {
                !product.product.trim() ? <span class="hljs-literal">null</span> : productsList = productsList.concat(product);
            }
        }

        <span class="hljs-keyword">return</span> productsList;
    });

    ...
}

...
</code></pre>
<p>// traverse for brand and product names</p>
<p>After investigating the DOM, we see that each listed item is enclosed under an element with the selector <code>div[data-cel-widget^="search_result_"]</code> . This particular selector seeks out all <code>div</code> tags with the attribute <code>data-cel-widget</code> that have a value starting with <code>search_result_</code>. </p>
<p>Similarly, we map out the selectors for the parameters we require as listed. If you want to learn more about DOM traversal, you can check out this informative <a target="_blank" href="https://zellwk.com/blog/dom-traversals/">article</a> by Zell.</p>
<ul>
<li><strong>total listed items:</strong> <code>div[data-cel-widget^="search_result_"]</code> </li>
<li><strong>brand:</strong> <code>div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base</code> (<code>i</code> stands for the node number in <code>total listed items</code>)</li>
<li><strong>product:</strong> <code>div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base</code>  or <code>div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal</code> (<code>i</code> stands for the node number in <code>total listed items</code>)</li>
<li><strong>url:</strong> <code>div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal</code> (<code>i</code> stands for the node number in <code>total listed items</code>)</li>
<li><strong>image:</strong> <code>div[data-cel-widget="search_result_${i}"] .s-image</code> (<code>i</code> stands for the node number in <code>total listed items</code>)</li>
<li><strong>price:</strong> <code>div[data-cel-widget="search_result_${i}"] span.a-offscreen</code> (<code>i</code> stands for the node number in <code>total listed items</code>)</li>
</ul>
<blockquote>
<p><strong>Note:</strong> We wait for <code>div[data-cel-widget^="search_result_"]</code> selector named elements to be available on the page by using the <code>page.waitFor</code> method.</p>
</blockquote>
<p>Once the <code>page.evaluate</code> method is invoked, we can see the data we require logged.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/image-162.png" alt="Image" width="600" height="400" loading="lazy">
<em>It works! We have our API data ready to serve what we require</em></p>
<h3 id="heading-adding-automation-to-ease-flow">Adding Automation to Ease Flow</h3>
<p>So far we are able to navigate to a page, extract the data we need, and transform it into an API-ready form. That sounds all hunky-dory. </p>
<p>However, consider for a moment a case where you have to navigate to one URL from another by performing some actions – and then try to extract the data you need. </p>
<p>Would that make your life a little trickier? Not at all. Puppeteer can easily imitate user behavior. Time to add some automation to our existing use case.</p>
<p>Unlike in the previous example, we will go to the <code>amazon.in</code> homepage and search for 'Shirts'. It will take us to the products listing page and we can extract the data required from the DOM. Easy peasy. Let's look at the code.</p>
<pre><code class="lang-js">...

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchProductList</span>(<span class="hljs-params">url, searchTerm</span>) </span>{
    ...
    await page.goto(url, { <span class="hljs-attr">waitUntil</span>: <span class="hljs-string">'networkidle2'</span> });

    <span class="hljs-keyword">await</span> page.waitFor(<span class="hljs-string">'input[name="field-keywords"]'</span>);
    <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-function"><span class="hljs-params">val</span> =&gt;</span> <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'input[name="field-keywords"]'</span>).value = val, searchTerm);

    <span class="hljs-keyword">await</span> page.click(<span class="hljs-string">'div.nav-search-submit.nav-sprite'</span>);

    <span class="hljs-comment">// DOM traversal and data mapping logic</span>
    <span class="hljs-comment">// returns a productsList array</span>
    ...
}

fetchProductList(<span class="hljs-string">'https://amazon.in'</span>, <span class="hljs-string">'Shirts'</span>);
</code></pre>
<p>We can see that we wait for the search box to be available and then we add the <code>searchTerm</code> passed using <code>page.evaluate</code>. We then navigate to the products listing page by emulating the 'search button' click action and exposing the DOM.</p>
<p>The complexity of automation varies from use case to use case.</p>
<h3 id="heading-some-notable-gotchas-a-minor-heads-up">Some Notable Gotchas: A Minor Heads Up</h3>
<p>Puppeteer's API is pretty comprehensive but there are a few gotchas I came across while working with it. Remember, not all of these gotchas are directly related to puppeteer but tend to work better along with it.</p>
<ul>
<li>Puppeteer creates a Chrome browser instance as already mentioned. However, it is likely that some existing websites might block access if they suspect bot activity. There is this package called <code>[user-agents](https://www.npmjs.com/package/user-agents)</code> which can be used with puppeteer to randomize the user-agent for the browser.</li>
</ul>
<blockquote>
<p><strong>Note:</strong> Scraping a website lies somewhere in the grey areas of legal acceptance. I would recommend using it with caution and checking rules where you live. </p>
</blockquote>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(<span class="hljs-string">'puppeteer'</span>);
<span class="hljs-keyword">const</span> userAgent = <span class="hljs-built_in">require</span>(<span class="hljs-string">'user-agents'</span>);

...

const browser = <span class="hljs-keyword">await</span> puppeteer.launch({ <span class="hljs-attr">headless</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">defaultViewport</span>: <span class="hljs-literal">null</span> });
<span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();
<span class="hljs-keyword">await</span> page.setUserAgent(userAgent.toString());

...
</code></pre>
<ul>
<li>We came across <code>defaultViewport: null</code> when launching our Chrome instance and I had listed it as optional. This is because it comes in handy only when you are viewing the Chrome instance being launched. It prevents the website's width and height from being affected when it is rendered.</li>
<li>Puppeteer is not the ultimate solution when it comes to performance. You, as a developer, will have to optimize it to increase its performance efficiency through actions like throttling animations on the site, allowing only essential network calls, etc.</li>
<li>Remember to always end a puppeteer session by closing the Browser instance by using <code>browser.close</code>. (I happened to miss out on it in the first try) It helps end a running Browser Session.</li>
<li>Certain common JavaScript operations like <code>console.log()</code> will not work within the scope of the page methods. The reason being that the <a target="_blank" href="https://pptr.dev/#?product=Puppeteer&amp;version=v3.1.0&amp;show=api-class-browsercontext">page context/browser context</a> differs from the node context in which your application is running.</li>
</ul>
<p>These are some of the gotchas I noticed. If you have more, feel free to reach out to me with them. I would love to learn more. </p>
<p>Done? Let's run the application.</p>
<h2 id="heading-website-to-your-api-bringing-it-all-together">Website to Your API: Bringing it All Together</h2>
<p>The application is run in non-headless mode so you can witness what exactly happens. We will automate the navigation to the product listing page from which we obtain the data.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/ezgif.com-video-to-gif--1-.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<p>There. You have your own API consumable data setup from the website of your choice. All you need to do now is to wire this up with a server side framework like <a target="_blank" href="https://expressjs.com/"><code>express</code></a> and you are good to go.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>There is so much you can do with Puppeteer. This is just one particular use case. I would recommend that you spend some time to read the official documentation. I will be doing the same.</p>
<p>Puppeteer is used extensively in some of the largest organizations for automation tasks like testing and server side rendering, among others. </p>
<p>There is no better time to get started with Puppeteer than now. </p>
<p>If you have any questions or comments, you can reach out to me on <a target="_blank" href="https://www.linkedin.com/in/tarique-ejaz/">LinkedIn</a> or <a target="_blank" href="https://twitter.com/theguynameddate">Twitter</a>. </p>
<p>In the meantime, keep coding.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up a Continuous Integration Pipeline with GitHub Actions and Puppeteer ]]>
                </title>
                <description>
                    <![CDATA[ By Dor Shinar Lately I've added continuous integration to my blog using Puppeteer for end to end testing. My main goal was to allow automatic dependency updates using Dependabot. In this guide I'll show you how to create such a pipeline yourself. As ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/continuous-integration-with-github-actions-and-puppeteer/</link>
                <guid isPermaLink="false">66d45e3e264384a65d5a950e</guid>
                
                    <category>
                        <![CDATA[ Continuous Integration ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub Actions ]]>
                    </category>
                
                    <category>
                        <![CDATA[ puppeteer ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 27 Jan 2020 23:13:18 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/01/7i4mnqi4x5tl0rn204ab.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Dor Shinar</p>
<p>Lately I've added continuous integration to my blog using Puppeteer for end to end testing. My main goal was to allow automatic dependency updates using <a target="_blank" href="https://dependabot.com/">Dependabot</a>. In this guide I'll show you how to create such a pipeline yourself.</p>
<p>As my CI platform, I chose <a target="_blank" href="https://github.com/features/actions">Github Actions</a>, as it is super easy to work with. It also integrates beautifully with any Github repository you already have. The whole thing only took roughly two days of intermittent work, and I think the results are quite awesome.</p>
<p>I do want to give a shout-out to Nick Taylor, who published <a target="_blank" href="https://www.iamdeveloper.com/blog/2019-08-15-update-dependencies-with-dependabot-cypress-and-netlify/">his article on the subject</a>, and laid the ground work for my efforts here. I encourage you to read his article as well.</p>
<p>My tech stack is quite different though. I chose <a target="_blank" href="https://pptr.dev/">puppeteer</a> as my end-to-end framework for several reasons. The first is that it is written and maintained by the folks behind the Chrome dev tools, so I'm guaranteed a life-time of support (until Chrome dies out, which is not in the near future), and it is really easy to work with.</p>
<p>Another reason is that at home I'm working on a windows laptop with WSL (on which I'm running zshell with oh-my-zsh). Setting up cypress is quite a bit more difficult (although in our world nothing is impossible). Both reasons led me to choose puppeteer, and so far I'm not regretting it.</p>
<h2 id="heading-end-to-end-testing">End to end testing</h2>
<p>End to end (or E2E) tests are different from other types of automated tests. E2E tests simulate a real user, performing actions on the screen. This kind of test should help fill the blank space between "static" tests - such as unit tests, where you usually don't bootstrap the entire application - and component testing, which usually runs against a single component (or a service in a micro-service architecture).</p>
<p>By simulating user interaction you get to test the experience of using your application or service in the same way a regular user would experience it.</p>
<p>The mantra that we try to follow is that it does not matter if your code performs perfectly if the button the user should press is hidden due to some CSS quirk. The end result is that the user will never get to feel the greatness of your code.</p>
<h2 id="heading-getting-started-with-puppeteer">Getting started with puppeteer</h2>
<p>Puppeteer has a few configuration options that make it really awesome to use for writing and validating tests.</p>
<p>Puppeteer tests can run in a "head-full" state. This means you can open a real browser window, navigate to the site being tested, and perform actions on the given page. This way you - the developers writing the tests - can see exactly what happens in the test, what buttons are being pressed and what the resulting UI looks like. </p>
<p>The opposite of "head-full" would be headless, where puppeteer does not open a browser window, making it ideal for CI pipelines.</p>
<p>Puppeteer is quite easy to work with, but you'll be surprised with the number of actions you can perform using an automated tool.</p>
<p>We'll start with a basic scraper that prints the page title when we go to <a target="_blank" href="https://dorshinar.me">https://dorshinar.me</a>. In order to run puppeteer tests, we must install it as a dependency:</p>
<pre><code class="lang-bash">npm i puppeteer
</code></pre>
<p>Now, our basic scraper looks like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(<span class="hljs-string">"puppeteer"</span>);

(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch();
  <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();
  <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">"https://dorshinar.me"</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-keyword">await</span> page.title());

  <span class="hljs-keyword">await</span> browser.close();
})();
</code></pre>
<p>What we do here is very simple: we open the browser with <code>puppeteer.launch()</code>, create a new page with <code>browser.newPage()</code> and navigate to this blog with <code>page.goto()</code>, and then we print the title. </p>
<p>There are a bunch of things we can do with the puppeteer API, such as:</p>
<p>Running code in the context of the page:</p>
<pre><code class="lang-js">(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> page.evaluate(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".awesome-button"</span>).click());
})();
</code></pre>
<p>Clicking on elements in the screen using a CSS selector:</p>
<pre><code class="lang-js">(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> page.click(<span class="hljs-string">".awesome-button"</span>);
})();
</code></pre>
<p>Making use of the <code>$</code> selector (jQuery style):</p>
<pre><code class="lang-js">(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> page.$(<span class="hljs-string">".awesome-button"</span>);
})();
</code></pre>
<p>Taking a screenshot:</p>
<pre><code class="lang-js">(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> page.screenshot({ <span class="hljs-attr">path</span>: <span class="hljs-string">"screenshot.png"</span> });
})();
</code></pre>
<p>There is a bunch more you can do with the puppeteer API, and I suggest you take a look at it before diving into writing tests. But the examples I've shown should give you a solid foundation to build from.</p>
<h3 id="heading-integrating-puppeteer-with-jest">Integrating puppeteer with Jest</h3>
<p><a target="_blank" href="https://jestjs.io/">jest</a> is an awesome test runner and assertion library. From their docs:</p>
<blockquote>
<p>Jest is a delightful JavaScript Testing Framework with a focus on simplicity.</p>
</blockquote>
<p>Jest allows you to run tests, mock imports, and make complex assertions really easily. Jest is also bundled with create-react-app, so I use it often at work.</p>
<h4 id="heading-writing-your-first-jest-test">Writing your first Jest test</h4>
<p>Jest tests are super easy to write, and they might be familiar to those who know other testing frameworks (as Jest uses <code>it</code>, <code>test</code>, <code>describe</code> and other familiar conventions).  </p>
<p>A basic test could look like:</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">subtract</span>(<span class="hljs-params">a, b</span>) </span>{
  <span class="hljs-keyword">return</span> a - b;
}

it(<span class="hljs-string">"subtracts 4 from 6 and returns 2"</span>, <span class="hljs-function">() =&gt;</span> {
  expect(subtract(<span class="hljs-number">6</span>, <span class="hljs-number">4</span>)).toBe(<span class="hljs-number">2</span>);
});
</code></pre>
<p>You can also group multiple tests under one <code>describe</code>, so you can run different describes or use it for convenient reporting:</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">divide</span>(<span class="hljs-params">a, b</span>) </span>{
  <span class="hljs-keyword">if</span> (b === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Can't divide by zero!"</span>);
  }
  <span class="hljs-keyword">return</span> a / b;
}

describe(<span class="hljs-string">"divide"</span>, <span class="hljs-function">() =&gt;</span> {
  it(<span class="hljs-string">"throws when dividing by zero"</span>, <span class="hljs-function">() =&gt;</span> {
    expect(<span class="hljs-function">() =&gt;</span> divide(<span class="hljs-number">6</span>, <span class="hljs-number">0</span>)).toThrow();
  });
  it(<span class="hljs-string">"returns 3 when dividing 6 by 3"</span>, <span class="hljs-function">() =&gt;</span> {
    expect(divide(<span class="hljs-number">6</span>, <span class="hljs-number">3</span>)).toBe(<span class="hljs-number">2</span>);
  });
});
</code></pre>
<p>You can, of course, create much more complicated tests using mocks and other type of assertions (or expectations), but for now that's enough.</p>
<p>Running the tests is also very simple:</p>
<pre><code class="lang-bash">jest
</code></pre>
<p>Jest will look for test files with any of the following popular naming conventions:</p>
<ul>
<li>Files with <code>.js</code> suffix in <code>__tests__</code> folders.</li>
<li>Files with <code>.test.js</code> suffix.</li>
<li>Files with <code>.spec.js</code> suffix.</li>
</ul>
<h4 id="heading-jest-puppeteer">jest-puppeteer</h4>
<p>Now, we need to make puppeteer play nicely with jest. This isn't a particularly hard job to do, as there is a great package named <a target="_blank" href="https://github.com/smooth-code/jest-puppeteer">jest-puppeteer</a> that comes to our aid.  </p>
<p>First, we must install it as a dependency:</p>
<pre><code class="lang-bash">npm i jest-puppeteer
</code></pre>
<p>And now we must extend our jest configuration. If you don't have one yet, there are a number of ways to do it. I'll go with a config file. Create a file named <code>jest.config.js</code> in the root of your project:</p>
<pre><code class="lang-bash">touch jest.config.js
</code></pre>
<p>In the file we must tell jest to use <code>jest-puppeteer</code>'s preset, so add the following code to the file:</p>
<pre><code class="lang-js{2}">module.exports = {
  preset: "jest-puppeteer"
  // The rest of your file...
};
</code></pre>
<p>You may specify a special launch configuration in a <code>jest-puppeteer.config.js</code> file, and jest-puppeteer will pass this configuration to <code>puppeteer.launch()</code>. For example:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">launch</span>: {
    <span class="hljs-attr">headless</span>: process.env.CI === <span class="hljs-string">"true"</span>,
    <span class="hljs-attr">ignoreDefaultArgs</span>: [<span class="hljs-string">"--disable-extensions"</span>],
    <span class="hljs-attr">args</span>: [<span class="hljs-string">"--no-sandbox"</span>],
    <span class="hljs-attr">executablePath</span>: <span class="hljs-string">"chrome.exe"</span>
  }
};
</code></pre>
<p><code>jest-puppeteer</code> will take care of opening a new browser and a new page and store them on the global scope. So in your tests you can simply use the globally available <code>browser</code> and <code>page</code> objects.</p>
<p>Another great feature we can use is the ability of jest-puppeteer to run your server during your tests, and kill it afterwards, with the <code>server</code> key:</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">launch</span>: {},
  <span class="hljs-attr">server</span>: {
    <span class="hljs-attr">command</span>: <span class="hljs-string">"npm run serve"</span>,
    <span class="hljs-attr">port</span>: <span class="hljs-number">9000</span>,
    <span class="hljs-attr">launchTimeout</span>: <span class="hljs-number">180000</span>
  }
};
</code></pre>
<p>Now jest-puppeteer will run <code>npm run serve</code>, with a timeout of 180 seconds (3 minutes), and listen on port 9000 to see when it will be up. Once the server starts the tests will run.</p>
<p>You can now write a full test suite using jest and puppeteer. The only thing left is creating a CI pipeline, for which we'll use GitHub actions.</p>
<p>You can add a script to your <code>package.json</code> file to execute your tests:</p>
<pre><code class="lang-json{3}">{
  "scripts": {
    "test:e2e": "jest"
  }
}
</code></pre>
<h2 id="heading-github-actions-in-a-gist">Github Actions in a gist</h2>
<p>Recently, Github released a big new feature called Actions. Basically, actions allow you to create workflows using plain yaml syntax, and run them on dedicated virtual machines. </p>
<p>In your workflow you can do pretty much anything you want, from basic <code>npm ci &amp;&amp; npm build &amp;&amp; npm run test</code> to more complicated stuff.</p>
<p>I'll show you how to configure a basic workflow running your puppeteer test suite, and prevent merging if your tests don't pass.</p>
<p>The easiest way to start is to click on the <code>Actions</code> tab in your github repo. If you haven't configured any action before, you'll see a list of previously configured workflows, from which you can choose one with some predefined configuration.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/github-actions-start-3.png" alt="github-actions-start-3" width="600" height="400" loading="lazy"></p>
<p>For our case, choosing the predefined Node.js action is good enough. The generated yaml looks like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Node</span> <span class="hljs-string">CI</span>

<span class="hljs-attr">on:</span> [<span class="hljs-string">push</span>]

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">node-version:</span> [<span class="hljs-number">8.</span><span class="hljs-string">x</span>, <span class="hljs-number">10.</span><span class="hljs-string">x</span>, <span class="hljs-number">12.</span><span class="hljs-string">x</span>]

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v1</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Use</span> <span class="hljs-string">Node.js</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.node-version</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">node-version:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.node-version</span> <span class="hljs-string">}}</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install,</span> <span class="hljs-string">build,</span> <span class="hljs-string">and</span> <span class="hljs-string">test</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          npm ci
          npm run build --if-present
          npm test
</span>        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">CI:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>In the file you can configure the workflow name, jobs to run, and when to run the workflow. You can run your workflow on every push, on new pull requests, or as a recurring event. </p>
<p>Jobs in a workflow run in parallel by default, but can be configured to run in sequence. In the above workflow, there is one job named <code>build</code>.</p>
<p>You can also choose the OS on which your workflow will run (by default you can use Windows Server 2019, Ubuntu 18.04, Ubuntu 16.04 and macOS Catalina 10.15 - at the time of publishing) with the <code>runs-on</code> key.</p>
<p>The <code>strategy</code> key can help us run our tests on a matrix of node versions. In this case we have the latest versions of the latest LTS majors - <code>8.x</code>, <code>10.x</code> and <code>12.x</code>. If you are interested in that you can leave it as is, or simply remove it and use any specific version you want.</p>
<p>The most interesting configuration option is the <code>steps</code>. With it we define what actually goes on in our pipeline. </p>
<p>Each step represents an action you can perform, such as checking out code from the repo, setting up your node version, installing dependencies, running tests, uploading artifacts (to be used later or downloaded) and many more. </p>
<p>You can find a very extensive list of readily available actions in the <a target="_blank" href="https://github.com/marketplace?type=actions">Actions Marketplace</a>.</p>
<p>The basic configuration will install dependencies, build our project and run our tests. If you need more (for example if you want to serve your application for e2e tests) you may alter it to your liking. Once done, commit your changes and you are good to go.</p>
<h3 id="heading-forcing-checks-to-pass-before-merge">Forcing checks to pass before merge</h3>
<p>The only thing left for us is to make sure no code can be merged before our workflow passes successfully. For that, go to your repo's settings and click on Branches:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/github-settings-branch-1.png" alt="Github Settings > Branch" width="600" height="400" loading="lazy"></p>
<p>We need to set a <strong>Branch protection rule</strong> so that malicious code (or at least code that doesn't pass our tests) won't be merged. Click on <strong>Add rule</strong>, and under <strong>Branch name pattern</strong> put your protected branch (master, dev or whichever one you choose). Make sure <strong>Require status checks to pass before merging</strong> is checked, and you'll be able to choose which checks must pass:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/github-actions-protections-1.png" alt="Require status checks" width="600" height="400" loading="lazy"></p>
<p>Click on Save changes below, and you're good to go!</p>
<p>Thank you for reading!<br>This article was previously published on my blog: <a target="_blank" href="https://dorshinar.me/continuous-integration-with-github-actions-and-puppeteer">dorshinar.me</a>, If you want to read more content, you can check my blog as it would mean a lot to me.  </p>
<p>If you want to support me, you can <a href="https://ko-fi.com/L3L116P44" target="_blank"><img src="https://az743702.vo.msecnd.net/cdn/kofi4.png?v=2" alt="Buy Me a Coffee at ko-fi.com" width="600" height="400" loading="lazy"></a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to set up server-side rendering in React with Rails using Puppeteer ]]>
                </title>
                <description>
                    <![CDATA[ By Sitaram Shelke This post is co-authored by Hricha Kabir, my colleague at Altizon Systems. She was working on this task primarily. I had the chance to learn along the way. _Photo by [Unsplash](https://unsplash.com/@rvruggiero?utm_source=medium&utm... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/server-side-rendering-react-with-rails-using-puppeteer-cf5ec2697e88/</link>
                <guid isPermaLink="false">66c35e92b8711219e1e72def</guid>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ puppeteer ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Rails ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 21 Feb 2019 06:00:04 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/0*JDwIoDI39Uzjzp5c" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Sitaram Shelke</p>
<p>This post is co-authored by <a target="_blank" href="https://in.linkedin.com/in/hrichakabir">Hricha Kabir</a>, my colleague at <a target="_blank" href="http://www.altizon.com">Altizon Systems</a>. She was working on this task primarily. I had the chance to learn along the way.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/SFMTJgWJQqSDFXU-rUJrn4HkgCccugY1GpYh" alt="Image" width="800" height="533" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@rvruggiero?utm_source=medium&amp;utm_medium=referral" rel="noopener" target="_blank" title=""&gt;Robert V. Ruggiero on &lt;a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral" rel="noopener" target="<em>blank" title=")</em></p>
<p>We work on an IoT product focusing on manufacturing industries and build analytics reports. Most of the time, report design and information vary based on the user’s role who is going to consume it.</p>
<p>Example: A Director level person is interested in a consolidated report of a week, whereas a Department Head is interested in the statistics of a single day and Quality Manager is keen about shift data. If we further drill down the hierarchy, then the Production Manager will be the one who wants to look into live production data.</p>
<p>For live data, Production manager can open the Live Dashboard and monitor it, but the user who wants to see a consolidated report won’t prefer to login into Product and check out a report. Instead, they prefer scheduled email communication which will send a report based on scheduled time and repeat it forever until unless a user stopped this scheduled job.</p>
<p>So our task was to write a utility which periodically sends a particular report to a user via email on a daily basis or at a defined scheduled time. For scheduling, we use <a target="_blank" href="https://sidekiq.org/">Sidekiq</a>, but our main challenge was: how to execute/calculate the report on the server side (without opening a browser) and send it over email.</p>
<p>In all of our products, we use <strong>React</strong> for the front-end and it does the heavy lifting of rendering the report on the browser. To achieve attaching the same report in the email, we would have to design an HTML template for every report. That means we would have had 2 views of each report, one written in HTML and another in ReactJS.</p>
<p>This has following drawbacks:</p>
<p><strong>1.</strong> The product code base will have duplicate code which violates the DRY principle for software development.</p>
<p><strong>2.</strong> Increases the time for designing a report which impacts product delivery.</p>
<p>We decided to look at other possible solutions instead of writing the same code twice. And we came across following options and explored them one by one until we succeeded in achieving our goal.</p>
<p><strong>1. Print Screen:</strong> Print the report using print screen and send it as an attachment.</p>
<p><strong>2. FrontEnd Print Button:</strong> In report’s UI, add a Print Button which will convert react component into HTML/PDF page.</p>
<p><strong>3. Ruby Gem:</strong> There is a ruby gem which can render a report component on the server side within Rails, generate its HTML and send it as an email body.</p>
<p><strong>4. Server-Side Rendering with Node server:</strong> Use serverside rendering using ReactDOMServer and <a target="_blank" href="https://developers.google.com/web/updates/2017/04/headless-chrome">headless</a> browser protocol to render HTML and JS and generate a pdf.</p>
<p>Now we will go through the details of each solution.</p>
<h4 id="heading-print-screen"><strong>Print Screen:</strong></h4>
<p>It was the simplest solution we had and it requires user input. A user needs to be present with a system who will do a screen capture as an image and attach it over the mail. But this doesn’t scale. Also what if a user wants a report at a scheduled time or sent periodically?</p>
<p>Example<strong>:</strong> Every week at 8 pm. It’s not possible for a person to do this. This also suffers from the inability to fully capture a scrollable page, so we had to drop this idea.</p>
<h4 id="heading-frontend-print-button"><strong>FrontEnd Print Button:</strong></h4>
<p>After doing some search, we found out there are some browser extensions available which produce a full image of an opened page. Example: Full Page Screen Capture in Chrome. After adding this extension to the browser, we would be able to capture any screen in image(png/jpg) format.</p>
<p>Again this solution requires no development but suffers from some of the previous scaling and scheduling issues. At the same time, we would still need a user logged in to the browser to perform this action, which defeats the purpose of email delivery.</p>
<h4 id="heading-rubygem"><strong>RubyGem:</strong></h4>
<p>Soon we realized that our solution cannot require browser interaction. We would need to use <a target="_blank" href="https://alligator.io/react/server-side-rendering/">server-side rendering</a>. So we started exploring Ruby Gems which support server-side rendering with React. We explored the following Gems</p>
<p><strong>3.1 <a target="_blank" href="https://github.com/aaronvb/rails_react_stdio">rails_react_stdio</a>:</strong></p>
<p>It is based on <a target="_blank" href="https://github.com/ReactTraining/react-stdio">react-stdio</a> which supports server-side rendering irrespective of server-side technology. It acts as a binary which will do the work of rendering react components. For rendering React Component on the server side we need to pass the file path of the react component and props if required. It will return a JSON response which will have the HTML code of the report. Further we can send HTML content over email.</p>
<p>Internally this gem uses <em>popen3</em> for executing render command. But this means that <em>react-stdio</em> binary needs to be present in the docker container where our rails app is running.</p>
<p>This is not great from a point of view of maintainability and reproducibility. Additionally having large HTML content with charts can be slow to load so we preferred pdf attachment. Yet we gave it a try.</p>
<p>Example<strong>:</strong> For rendering a report which has the component <em>TestComponent</em> and the file path <em>app/assets/javascripts/components/TestComponent.jsx</em></p>
<p>First, include the gem in Gemfile:</p>
<pre><code class="lang-rb">gem ‘rails_react_stdio’, ‘~&gt; <span class="hljs-number">0</span>.<span class="hljs-number">1.0</span>’
</code></pre>
<p>Now from the email scheduler call the gem method to render the report and get the HTML from the response. Then send this response to email.</p>
<pre><code class="lang-rb">email_body = RailsReactStdio::React.render(‘app/assets/javascripts/components/TestComponent.jsx’, {<span class="hljs-symbol">city:</span> “Pune”})
</code></pre>
<p>We tried using the above method but had no success. Also the GitHub repo was not actively maintained, and all of its test cases were failing. So we decided to move forward without this.</p>
<p><strong>3.2 react-rails:</strong></p>
<p>The ReactJS community built this gem. It uses <a target="_blank" href="https://github.com/rails/execjs">ExecJS</a> for executing render-action of a react component on the server side. We just need to pass one flag <em>‘prerender’: true</em>.</p>
<pre><code class="lang-js">&lt;%= react_component(‘Dashboard’, {<span class="hljs-attr">name</span>: ‘Example’}, {<span class="hljs-attr">prerender</span>: <span class="hljs-literal">true</span>}) %&gt;
</code></pre>
<p>This prerendering process does not have access to the window or document so it does not load runtime JavaScript or CSS. We also use JQuery for a few things so it wouldn’t work as well.</p>
<p>There is an alternative: this gem has another class for server-side rendering, <em>ExecJSRenderer</em>, which helps in availing JavaScript to a component on the server side.</p>
<p><em>ExecJSRenderer</em> Class has 2 methods: _before<em>render</em> and _after<em>render,</em> which gives access to the JavaScript required before and after component rendering. But it’d require a lot of changes in the existing code base for supporting server-rendering, in every controller. Apart from this, ExecJS doesn’t provide sandboxing as well as runtime error information. We were still looking for something better.</p>
<h4 id="heading-server-side-rendering-with-node-server"><strong>Server-Side Rendering with Node server:</strong></h4>
<p>Most of the ruby gems we explored internally created the node server and rendered react on the server side. So instead of using Ruby, we decided to directly use the node server and achieve this task ourselves.</p>
<p><strong>4.1 ReactDOMServer based solution:</strong></p>
<p>Here we use <a target="_blank" href="https://reactjs.org/docs/react-dom-server.html">ReactDOMServer</a>. It is the preferred solution for server-side rendering from the React team. We created a node server which calls the <em>renderToString()</em> method with a react component. It returns the rendered content which we combine with HTML and send over email.</p>
<p>Example<strong>:</strong></p>
<pre><code class="lang-js">server.get(‘/’, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-comment">/**
  * renderToString() will take our React app and turn it into a      string
  * to be inserted into our Html template function.
  */</span>
  <span class="hljs-built_in">console</span>.log(‘started’)
  <span class="hljs-keyword">const</span> body = renderToString(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">App</span> /&gt;</span></span>);
  <span class="hljs-keyword">const</span> title = ‘Server side Rendering React Components’;
  <span class="hljs-keyword">var</span> result = Html({ body, title })
}
</code></pre>
<p>The <em>renderToString()</em> method returns a string response. We pass this response to an HTML template and send the template over mail.</p>
<p>When we tested this out, an email was received as expected except all the images used in the report were broken. The email was not able to resolve the image source relative path.</p>
<p>To get the correct images in the email, we would either need to</p>
<ul>
<li>Store images in S3 and use the source URL in the report: So now we would be adding S3 image URLs in the email, and the email server would directly load images from the S3 server. It would require extra cloud space for storing images, and downloading from destination requires another network call from the email inbox.</li>
</ul>
<p>Or</p>
<ul>
<li>Send base64 code of image in the mail: Instead of image URL, we can send the base64 code of an image. Although It increases network payload, many mail servers such as Outlook and Gmail block base64 images.</li>
</ul>
<p>So we would still need to do something about this.</p>
<p><strong>4.2 <a target="_blank" href="https://github.com/GoogleChrome/puppeteer">Puppeteer</a>:</strong></p>
<p>After exploring the above methods, we discovered Puppeteer Headless Chrome service. Puppeteer is a NodeJS library from the Google Chrome team, used in end to end testing. By default, it uses the Chrome/Chromium browser for the same. Essentially it simulates all the actions a user can perform in the browser. Example: Keyboard Input, Mouse events, Form submission etc.</p>
<p>The result of a puppeteer request can be an HTML page, Screenshot, or PDF. In case of HTML, it renders the full page on the server side along with all images, CSS, and JavaScript. Or a user can render a page on the server side and if required they can generate a screenshot or a pdf.</p>
<p>If we use this library with a Node server, we can schedule a task within Sidekiq which would make a request to this server, render a report, and send it over email. Puppeteer also has a rich set of APIs which support in sending customizing headers in the request which help us in authenticating a background request.</p>
<p>This is exactly what we needed!</p>
<p><strong>So the overall request lifecycle inside puppeteer should look like this:</strong></p>
<p><strong>1.</strong> It launches the browser</p>
<p><strong>2.</strong> Creates a page on the browser</p>
<p><strong>3.</strong> Authenticate with the Report Application server</p>
<p><strong>4.</strong> Opens the report URL that the user wants and returns the rendered page content</p>
<p><strong>5.</strong> Based on the user’s requirement, stores HTML of the rendered page or takes a Screenshot or generates a PDF.</p>
<p>We can use either the default browser (gets downloaded when we install puppeteer) or we can give a specific browser version. If we give a specific browser version, then we will have to make sure that puppeteer APIs are compatible with the given browser.</p>
<p>All of the above can be achieved using the following steps.</p>
<p><strong>1. Install puppeteer:</strong></p>
<p>It downloads the default Chrome/Chromium browser. So If we want to launch the default browser and install puppeteer:</p>
<pre><code class="lang-bash">npm install puppeteer
</code></pre>
<p><strong>2. Serve a Request:</strong></p>
<p>Build a server which will receive a request and implement a request lifecycle and return the desired result.</p>
<p>The following is the code snippet we use for generating a pdf of a given page URL.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> puppeteer = <span class="hljs-built_in">require</span>(“puppeteer”);
<span class="hljs-keyword">const</span> browser = <span class="hljs-keyword">await</span> puppeteer.launch();
<span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> browser.newPage();
<span class="hljs-keyword">await</span> page.emulateMedia(“screen”);
<span class="hljs-keyword">await</span> page.goto(‘https:<span class="hljs-comment">//www.google.com', {</span>
      timeout: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>, 
      <span class="hljs-attr">waitUntil</span>: “networkidle0”
});
<span class="hljs-keyword">await</span> page.pdf(pdfOptions);
<span class="hljs-keyword">return</span> page;
</code></pre>
<p>As we already mentioned in the lifecycle steps above, it first launches the browser. Then it creates and opens a page in the launched browser. Here along with the URL, we have passed <em>timeout</em> and <em>waitUntil</em> params which are given for the following reasons:</p>
<ul>
<li><em>timeout</em>: If we want to restrict a request time then we can pass it to timeout variable</li>
<li><em>waitUntil</em>: if networkidle0 is given, then the next request will not be served until or unless the current one is completed.</li>
</ul>
<p>In the end, rendered content will be passed to the pdf method and it will generate a pdf. We can also provide PDF formatting options like page height, page width, headers, footers, and margins.</p>
<p>Additionally, we have called page.emulateMedia(“screen”); which applies CSS to the page. If we don’t add emaulateMedia, our PDF doesn’t load the CSS.</p>
<p>Apart from what we have used, there are many different configuration options with Puppeteer APIs. Visit the <a target="_blank" href="https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#">API docs</a> for more information.</p>
<p>If you found this helpful or if you have any suggestions, please feel free to write them it in comments.</p>
<p>That’s all folks.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
