ScreenshotNeo

BlogHow-to

How to Install a PWA with Puppeteer

Puppeteer can automate browser interactions, but it does not provide a universal PWA installer. Set up Puppeteer, check installability, and handle supported install flows safely.

By the ScreenshotNeo team4 October 20268 min read

Short answer: Puppeteer does not have a universal API that installs any Progressive Web App (PWA). It can open a browser, inspect the app, and automate page interactions. The browser and platform decide whether an install route is available. For supported browsers, your app can expose an install button backed by beforeinstallprompt; Puppeteer can click that button, but that does not guarantee that a browser or operating-system install dialog can be completed in automation.

There are three separate tasks: install Puppeteer and its browser, make the PWA eligible under the target browser’s current rules, and test the browser-supported install flow. Keep those tasks distinct so a successful page interaction is not mistaken for a completed installation.

1. Install Puppeteer and its browser

The examples below use JavaScript modules and Puppeteer’s bundled Chrome for Testing. Run them from a Node.js project:

mkdir pwa-install-check
cd pwa-install-check
npm init -y
npm install puppeteer

The puppeteer package downloads a compatible Chrome for Testing browser by default. puppeteer-core does not download a browser; use it when you manage the browser binary separately. If package-manager scripts are blocked and the browser download was skipped, install a browser with:

npx puppeteer browsers install

Puppeteer runs headless by default. Set headless: false when you need to inspect a visible browser manually. Headed mode alone does not establish that native install UI is supported or automatable in your environment.

2. Check the PWA in the target browser

Before automating installation, verify that the site is served in the way the target browser expects and that its manifest and other requirements are satisfied. A manifest or service worker on its own does not prove that the browser will offer installation.

Chrome’s legacy Lighthouse installability checklist included an app name or short name, 192×192 and 512×512 icons, a start URL, an eligible display mode, and prefer_related_applications not set to true. Chrome warns that a manifest alone is insufficient and that Lighthouse’s PWA testing is deprecated. Treat this as a checklist for investigation, not a universal or current guarantee. Check the current criteria for the exact browser and platform you target.

Also distinguish two outcomes:

  • Page-level readiness: the app has the metadata and behavior the target browser requires.
  • Successful installation: the browser or platform actually completes its install flow.

A Puppeteer assertion about the page can verify the first. It cannot, by itself, prove the second.

3. Add an install button for supported browsers

In browsers that support beforeinstallprompt, wait for the browser to signal that installation is available. Save that event and call its prompt() only from a user gesture, such as a click. The event may never fire: the app might not be eligible, might already be installed, or the browser and platform might not support this path. A saved event is for one prompt only.

This minimal page code hides the button until the browser reports that the prompt is available. Replace the placeholder text and styling to fit your app:

<button id="install-app" type="button" hidden>Install app</button>
<p id="install-status" role="status"></p>
<script>
  const installButton = document.querySelector('#install-app');
  const status = document.querySelector('#install-status');
  let installEvent;

  window.addEventListener('beforeinstallprompt', (event) => {
    // Keep the browser's default prompt from appearing immediately.
    event.preventDefault();
    installEvent = event;
    installButton.hidden = false;
  });

  installButton.addEventListener('click', async () => {
    if (!installEvent) {
      status.textContent = 'Installation is not available in this browser.';
      installButton.hidden = true;
      return;
    }

    const event = installEvent;
    installEvent = undefined;
    installButton.disabled = true;

    try {
      await event.prompt();
      const choice = await event.userChoice;
      status.textContent = `Install prompt outcome: ${choice.outcome}`;
    } catch (error) {
      status.textContent = 'The install prompt could not be opened.';
      console.error(error);
    } finally {
      installButton.hidden = true;
      installButton.disabled = false;
    }
  });
</script>

The outcome reports what happened to the prompt; do not treat it as proof that a Puppeteer-driven test installed the app on the operating system. For robust UI, also handle the case where the event never fires and explain the appropriate manual route for the user’s browser.

4. Automate the supported page interaction with Puppeteer

The following runnable script navigates to the app and clicks its install button if the browser exposes it. It reports whether the app offered the custom button and what the page’s handler reports. It does not claim to verify a completed native installation.

// save as install-check.mjs
import puppeteer from 'puppeteer';

const appUrl = process.env.APP_URL;
if (!appUrl) {
  throw new Error('Set APP_URL to the PWA URL you want to check.');
}

const browser = await puppeteer.launch({ headless: false });
try {
  const page = await browser.newPage();
  page.setDefaultTimeout(10_000);

  await page.goto(appUrl, { waitUntil: 'networkidle2' });
  await page.waitForSelector('#install-app', { visible: true, timeout: 5_000 })
    .catch(() => null);

  const button = page.locator('#install-app');
  const available = await button.isVisible().catch(() => false);
  if (!available) {
    console.log('No app-controlled install button appeared.');
    console.log('Check browser/platform support, app eligibility, and installed state.');
  } else {
    await button.click();
    await page.waitForFunction(() => {
      const status = document.querySelector('#install-status');
      return status?.textContent?.length > 0;
    }, { timeout: 10_000 }).catch(() => null);
    console.log('Page status:', await page.$eval(
      '#install-status', (element) => element.textContent
    ));
  }

  console.log('Browser version:', await browser.version());
  console.log('Page URL:', page.url());
} finally {
  await browser.close();
}

Run it with your app URL:

APP_URL=https://example.com node install-check.mjs

Use a URL you control or are authorized to test. Record the browser version, operating system, and whether the browser was headed or headless with any test results. The Puppeteer documentation establishes page navigation and interaction, but does not establish that headless mode triggers browser or OS installation UI.

5. Choose the right install path for the platform

  • Supported Chromium browser: test whether the app’s button appears after beforeinstallprompt. If it does not, investigate eligibility, browser support, and installed state rather than showing a fake prompt.
  • Already-installed app: the install event may not be available. Treat the absent event as a normal state and avoid repeatedly prompting.
  • iOS or iPadOS: Chrome and Edge on these platforms do not support PWA installation through beforeinstallprompt. For an installable PWA, direct users to Safari’s Share then Add to Home Screen flow. A Chromium event test does not cover this route.
  • Other browsers or platforms: check that browser’s current installation instructions and APIs. Do not infer support from behavior in Chrome on a different operating system.

Newer install mechanisms also need version-specific checking. A Chrome for Developers report published May 12, 2026 described the <install> element behind a flag in Chrome or Edge 148 and as an origin trial in versions 148 through 153; it also described a separate origin trial for navigator.install(). Those are experimental, time-bounded availability details, not general support guarantees. Check current browser documentation before adopting them.

6. Troubleshooting

Symptom Likely cause What to do
Cannot find package 'puppeteer' The script is outside the project or the package was not installed. Run npm install puppeteer in the project directory and run the script from that project.
Browser executable missing The browser download was skipped, or you installed puppeteer-core without configuring a browser. For Puppeteer, run npx puppeteer browsers install. For puppeteer-core, install and configure a compatible browser binary separately.
Install button never appears beforeinstallprompt did not fire because of eligibility, support, or installed state. Inspect the app against the target browser’s current criteria, confirm browser/platform support, and provide the platform’s manual instructions.
Prompt code throws or cannot be reused The stored event was absent or had already been used. Only call prompt() after receiving the event, invoke it from the button’s user gesture, and discard it after that attempt.
Test hangs at navigation networkidle2 may not occur on pages with persistent requests, or the site may be slow. Use a suitable readiness condition such as domcontentloaded followed by a selector wait, and keep explicit timeouts.
Headless test passes but no app is installed The script verified page behavior, not native browser or OS installation. Run a separately defined manual or platform-specific installation check. Report browser, version, platform, and mode.
iOS browser has no custom prompt Chrome and Edge on iOS/iPadOS do not support the event-based install route. Explain the Safari Share and Add to Home Screen steps for the user.

7. Reliability, performance, and cost

Browser installation is interactive and platform-dependent, so make tests assert observable states rather than assume the browser will show a native prompt. Keep separate checks for page eligibility, visibility of your app’s install control, and actual user installation. The latter may require a manual or platform-specific test environment.

For faster and less flaky page checks, wait for the app control or another specific readiness signal instead of using a long fixed sleep. Pages that keep network connections open may not reach networkidle2; use domcontentloaded plus targeted selector waits in that case. Close the browser in a finally block, as in the example, so failures do not leave browser processes running.

With puppeteer, the compatible browser download consumes disk space and browser startup consumes time and memory. With puppeteer-core, you manage browser installation and compatibility yourself. The cited Puppeteer and PWA sources do not establish a general per-install cost or performance benchmark; budget based on your own CI environment and test volume.

Or skip the browser setup

If your goal is to capture how the PWA page looks, rather than install it, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Screenshot capture shows a page; it does not install a PWA. Create a free account for 1,000 screenshots a month, with no card required.

FAQ

Can Puppeteer install every PWA automatically?

No. Puppeteer automates browser interactions, while install availability and completion depend on browser, platform, app eligibility, and browser state.

How do I trigger the PWA install prompt?

In browsers that support beforeinstallprompt, store the event when it fires and call its prompt() from a user gesture. The event may not fire, and it can be used once.

Can I test iPhone installation with the same flow?

No. Use Safari’s Share and Add to Home Screen route for installable PWAs on iOS and iPadOS; the cited guidance says Chrome and Edge there do not support installation through beforeinstallprompt.

Sources