Repository: elastic/synthetics
Pull request #374 (2021-09-13 14:33:49)

Capture and log page errors
Fixes https://github.com/elastic/synthetics/issues/226

Add an event handler in browser console for "pageerror".

Capturing `error.message` and `error.stack.toString()`



apmmachine: 
## :grey_exclamation: Build Aborted
  
  > Either there was a build timeout or someone aborted the build.'}
  


<!-- BUILD BADGES-->
> _the below badges are clickable and redirect to their specific view in the CI or DOCS_
[![Pipeline View](https://img.shields.io/badge/pipeline-pipeline%20-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-374/detail/PR-374/1//pipeline) [![Test View](https://img.shields.io/badge/test-test-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-374/detail/PR-374/1//tests) [![Changes](https://img.shields.io/badge/changes-changes-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-374/detail/PR-374/1//changes) [![Artifacts](https://img.shields.io/badge/artifacts-artifacts-yellow)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-374/detail/PR-374/1//artifacts)   [![preview](https://img.shields.io/badge/elastic-observability-blue)](https://ci-stats.elastic.co/app/apm/services/apm-ci/transactions/view?rangeFrom=2021-09-13T14:24:04.838Z&rangeTo=2021-09-13T14:44:04.838Z&transactionName=apm-agent-rum/elastic-synthetics/PR-{number}&transactionType=unknown&latencyAggregationType=avg&traceId=bd429518ad5ff0750c139aee783a47ec&transactionId=7635b3d7404d906a)

<!-- BUILD SUMMARY-->
<details><summary>Expand to view the summary</summary>
<p>

#### Build stats




* Start Time: 2021-09-13T14:34:04.838+0000


* Duration: 28 min 51 sec


* Commit: aff47d4ed7e8215f92dc0b11d2798ec79e577acb



#### Test stats :test_tube:

| Test         | Results                         |
| ------------ | :-----------------------------: |
| Failed       | 0  |
| Passed       | 123  |
| Skipped      | 0 |
| Total        | 123   |


#### Trends :test_tube:


![Image of Build Times](https://apm-ci.elastic.co/job/apm-agent-rum/job/elastic-synthetics/job/PR-374/buildTimeGraph/png)


![Image of Tests](https://apm-ci.elastic.co/job/apm-agent-rum/job/elastic-synthetics/job/PR-374/1/testReport/history/countGraph/png?start=0&amp;end=25)


</p>
</details>

<!-- TEST RESULTS IF ANY-->


<!-- STEPS ERRORS IF ANY -->











<!-- LOG OUTPUT-->
### Log output

<details><summary>Expand to view the last 100 lines of log output</summary>
<p>

```
[2021-09-13T14:44:11.879Z]  > git merge e67a91dacc4c981aeb6b0f9d715f07b059da8f80 # timeout=10
[2021-09-13T14:44:11.901Z]  > git rev-parse HEAD^{commit} # timeout=10
[2021-09-13T14:44:11.918Z]  > git config core.sparsecheckout # timeout=10
[2021-09-13T14:44:11.926Z]  > git checkout -f aff47d4ed7e8215f92dc0b11d2798ec79e577acb # timeout=15
[2021-09-13T14:44:16.663Z] Commit message: "Capture and log page errors"
[2021-09-13T14:44:16.663Z] Cleaning workspace
[2021-09-13T14:44:16.665Z]  > git rev-parse --verify HEAD # timeout=10
[2021-09-13T14:44:16.675Z] Resetting working tree
[2021-09-13T14:44:16.675Z]  > git reset --hard # timeout=10
[2021-09-13T14:44:16.818Z]  > git --version # timeout=10
[2021-09-13T14:44:16.822Z]  > git --version # 'git version 2.17.1'
[2021-09-13T14:44:16.833Z] fatal: bad object b49e2725d04b2fd38a5b83307faca216df1ba780
[2021-09-13T14:44:16.695Z]  > git clean -fdx # timeout=10
[2021-09-13T14:44:18.804Z] Running in /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/src/github.com/elastic/synthetics
[2021-09-13T14:44:35.445Z] + set +x
[2021-09-13T14:44:35.446Z] => Downloading nvm from git to '/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/.nvm'
[2021-09-13T14:44:35.446Z] => Initialized empty Git repository in /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/.nvm/.git/
[2021-09-13T14:44:36.019Z] From https://github.com/nvm-sh/nvm
[2021-09-13T14:44:36.019Z]  * [new tag]         v0.37.1    -> v0.37.1
[2021-09-13T14:44:36.019Z] => Compressing and cleaning up git repository
[2021-09-13T14:44:36.019Z] 
[2021-09-13T14:44:36.019Z] => Profile not found. Tried ~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile.
[2021-09-13T14:44:36.019Z] => Create one of them and run this script again
[2021-09-13T14:44:36.019Z]    OR
[2021-09-13T14:44:36.019Z] => Append the following lines to the correct file yourself:
[2021-09-13T14:44:36.019Z] 
[2021-09-13T14:44:36.019Z] export NVM_DIR="$HOME/.nvm"
[2021-09-13T14:44:36.019Z] [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[2021-09-13T14:44:36.019Z] 
[2021-09-13T14:44:37.938Z] => Close and reopen your terminal to start using nvm or run the following to use it now:
[2021-09-13T14:44:37.938Z] 
[2021-09-13T14:44:37.938Z] export NVM_DIR="$HOME/.nvm"
[2021-09-13T14:44:37.938Z] [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[2021-09-13T14:44:37.938Z] Found '/var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/src/github.com/elastic/synthetics/.nvmrc' with version <14.17.5>
[2021-09-13T14:44:38.200Z] v14.17.5 is already installed.
[2021-09-13T14:44:38.462Z] Now using node v14.17.5 (npm v6.14.14)
[2021-09-13T14:44:38.462Z] /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374
[2021-09-13T14:44:38.946Z] Running in /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/src/github.com/elastic/synthetics
[2021-09-13T14:44:39.268Z] + chmod -R ugo+rw examples
[2021-09-13T14:44:39.300Z] Running in /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/src/github.com/elastic/synthetics/__tests__/e2e
[2021-09-13T14:44:39.623Z] + npm run test
[2021-09-13T14:44:39.624Z] 
[2021-09-13T14:44:39.624Z] > e2e-synthetics@ test /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/src/github.com/elastic/synthetics/__tests__/e2e
[2021-09-13T14:44:39.624Z] > npm i && npm run setup &&  bash scripts/test.sh
[2021-09-13T14:44:39.624Z] 
[2021-09-13T14:44:42.922Z] added 3 packages from 4 contributors and audited 184 packages in 2.715s
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] 69 packages are looking for funding
[2021-09-13T14:44:45.467Z]   run `npm fund` for details
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] found 0 vulnerabilities
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] > e2e-synthetics@ setup /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374/src/github.com/elastic/synthetics/__tests__/e2e
[2021-09-13T14:44:45.467Z] > bash scripts/setup.sh
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] Temporary folder
[2021-09-13T14:44:45.467Z] Temporary files will be stored in: ./tmp
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.467Z] Starting elasticsearch , kibana and synthetics docker
[2021-09-13T14:44:45.467Z] 
[2021-09-13T14:44:45.729Z] 
[2021-09-13T14:44:45.729Z] Waiting for synthetics docker to start...
[2021-09-13T14:44:45.729Z] Error: No such object: synthetics
[2021-09-13T14:44:55.760Z] Error: No such object: synthetics
[2021-09-13T14:45:05.760Z] Error: No such object: synthetics
[2021-09-13T14:45:17.986Z] Error: No such object: synthetics
[2021-09-13T14:45:26.121Z] Error: No such object: synthetics
[2021-09-13T14:45:36.117Z] Error: No such object: synthetics
[2021-09-13T14:45:46.146Z] ✅ Setup completed successfully. Running e2e tests...
[2021-09-13T14:45:47.556Z] Waiting for Elastic Search  to start
[2021-09-13T14:46:34.477Z] Waiting for Synthetics to send data to ES for test monitor
[2021-09-13T14:47:42.323Z] Waiting for kibana server to start
[2021-09-13T15:01:37.702Z] Cancelling nested steps due to timeout
[2021-09-13T15:01:37.705Z] Sending interrupt signal to process
[2021-09-13T15:01:49.992Z] scripts/test.sh: line 26:  6190 Terminated              SYNTHETICS_JUNIT_FILE='junit.xml' npx @elastic/synthetics uptime.journey.ts --reporter junit
[2021-09-13T15:01:49.993Z] npm ERR! code ELIFECYCLE
[2021-09-13T15:01:49.993Z] npm ERR! errno 143
[2021-09-13T15:01:49.993Z] npm ERR! e2e-synthetics@ test: `npm i && npm run setup &&  bash scripts/test.sh`
[2021-09-13T15:01:49.993Z] npm ERR! Exit status 143
[2021-09-13T15:01:49.993Z] npm ERR! 
[2021-09-13T15:01:49.993Z] npmTerminated
[2021-09-13T15:01:50.002Z] script returned exit code 143
[2021-09-13T15:01:51.492Z] Post stage
[2021-09-13T15:01:51.539Z] Archiving artifacts
[2021-09-13T15:01:51.970Z] Recording test results
[2021-09-13T15:01:52.384Z] None of the test reports contained any result
[2021-09-13T15:01:52.393Z] [Checks API] No suitable checks publisher found.
[2021-09-13T15:01:52.725Z] Stage "Release" skipped due to earlier failure(s)
[2021-09-13T15:01:52.775Z] Stage "Release" skipped due to earlier failure(s)
[2021-09-13T15:01:53.036Z] Stage "Release" skipped due to earlier failure(s)
[2021-09-13T15:01:54.461Z] Running on worker-1225339 in /var/lib/jenkins/workspace/nt-rum_elastic-synthetics_PR-374
[2021-09-13T15:01:54.553Z] [INFO] getVaultSecret: Getting secrets
[2021-09-13T15:01:54.611Z] Masking supported pattern matches of $VAULT_ADDR or $VAULT_ROLE_ID or $VAULT_SECRET_ID
[2021-09-13T15:01:56.869Z] + chmod 755 generate-build-data.sh
[2021-09-13T15:01:56.869Z] + ./generate-build-data.sh https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-rum/elastic-synthetics/PR-374/ https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-rum/elastic-synthetics/PR-374/runs/1 ABORTED 1670503
[2021-09-13T15:01:56.869Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-rum/elastic-synthetics/PR-374/runs/1/steps/?limit=10000 -o steps-info.json
[2021-09-13T15:01:57.568Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-rum/elastic-synthetics/PR-374/runs/1/tests/?status=FAILED -o tests-errors.json
[2021-09-13T15:01:57.568Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-rum/elastic-synthetics/PR-374/runs/1/log/ -o pipeline-log.txt

```

</p>
</details>



<!--COMMENT_GENERATED_WITH_ID_comment.id-->
shahzad31: @vigneshshanmugam i have added a unit test
shahzad31: > Looking good, can you add a test for promise rejection as well.

@vigneshshanmugam promoise rejections doesnt let the test run. I think current test cover enough scenerios.
vigneshshanmugam: > @vigneshshanmugam promoise rejections doesnt let the test run. I think current test cover enough scenerios.

Probably its a problem then, The intention of the original issue was to capture unhandled exceptions. Will merge once i test it out. 

Patch:
diff --git a/src/common_types.ts b/src/common_types.ts
index 2f70bd5..e6e446b 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -119,6 +119,7 @@ export type NetworkInfo = {
 export type BrowserMessage = {
   text: string;
   type: string;
+  error?: Error;
 } & DefaultPluginOutput;
 
 export type PluginOutput = {
diff --git a/src/plugins/browser-console.ts b/src/plugins/browser-console.ts
index f5ce280..82572ce 100644
--- a/src/plugins/browser-console.ts
+++ b/src/plugins/browser-console.ts
@@ -48,18 +48,41 @@ export class BrowserConsole {
         type,
         step: { name, index },
       });
-      if (this.messages.length > defaultMessageLimit) {
-        this.messages.splice(0, 1);
-      }
+
+      this.enforceMessagesLimit();
+    }
+  };
+
+  private pageErrorEventListener = (error: Error) => {
+    if (!this._currentStep) {
+      return;
     }
+    const { name, index } = this._currentStep;
+    this.messages.push({
+      timestamp: getTimestamp(),
+      text: error.message,
+      type: 'error',
+      step: { name, index },
+      error,
+    });
+
+    this.enforceMessagesLimit();
   };
 
+  private enforceMessagesLimit() {
+    if (this.messages.length > defaultMessageLimit) {
+      this.messages.splice(0, 1);
+    }
+  }
+
   start() {
     this.driver.page.on('console', this.consoleEventListener);
+    this.driver.page.on('pageerror', this.pageErrorEventListener);
   }
 
   stop() {
     this.driver.page.off('console', this.consoleEventListener);
+    this.driver.page.off('pageerror', this.pageErrorEventListener);
     return this.messages;
   }
 }
diff --git a/src/reporters/json.ts b/src/reporters/json.ts
index 08881a5..944c656 100644
--- a/src/reporters/json.ts
+++ b/src/reporters/json.ts
@@ -483,13 +483,17 @@ export default class JSONReporter extends BaseReporter {
           });
         }
         if (browserconsole) {
-          browserconsole.forEach(({ timestamp, text, type, step }) => {
+          browserconsole.forEach(({ timestamp, text, type, step, error }) => {
             this.writeJSON({
               type: 'journey/browserconsole',
               journey,
               timestamp,
               step,
-              payload: { text, type } as Payload,
+              error,
+              payload: {
+                text,
+                type,
+              } as Payload,
             });
           });
         }
@@ -554,7 +558,7 @@ export default class JSONReporter extends BaseReporter {
     });
   }
 
-  // Writes a structered synthetics event
+  // Writes a structured synthetics event
   // Note that blob is ultimately stored in ES as a base64 encoded string. You must base 64 encode
   // it before passing it into this function!
   // The payload field is an un-indexed field with no ES mapping, so users can put arbitary structured


Test patch:
diff --git a/__tests__/plugins/browser-console.test.ts b/__tests__/plugins/browser-console.test.ts
index 9a1884e..04edb8a 100644
--- a/__tests__/plugins/browser-console.test.ts
+++ b/__tests__/plugins/browser-console.test.ts
@@ -30,6 +30,7 @@ import { wsEndpoint } from '../utils/test-config';
 
 describe('BrowserConsole', () => {
   let server: Server;
+  const currentStep = { name: 'test-step', index: 0 };
   beforeAll(async () => {
     server = await Server.create();
   });
@@ -43,7 +44,7 @@ describe('BrowserConsole', () => {
     const { page } = driver;
     browserConsole.start();
     await page.goto(server.TEST_PAGE);
-    browserConsole._currentStep = { name: 'step-name', index: 0 };
+    browserConsole._currentStep = currentStep;
     await page.evaluate(() =>
       console.warn('test-message', 1, { test: 'test' })
     );
@@ -53,6 +54,58 @@ describe('BrowserConsole', () => {
     expect(testMessage.text).toEqual(`test-message 1 {test: test}`);
     expect(testMessage.type).toEqual('warning');
     expect(testMessage.timestamp).toBeDefined();
-    expect(testMessage.step).toEqual({ name: 'step-name', index: 0 });
+    expect(testMessage.step).toEqual(currentStep);
+  });
+
+  it('should capture browser page errors', async () => {
+    const driver = await Gatherer.setupDriver({ wsEndpoint });
+    const browserConsole = new BrowserConsole(driver);
+    const { page } = driver;
+    browserConsole.start();
+    await page.goto(server.TEST_PAGE);
+    browserConsole._currentStep = currentStep;
+    await page.setContent(`
+      <img src="imagefound.gif" onError="that.onerror=null;this.src='imagenotfound.gif';">
+   `);
+    await page.waitForLoadState('networkidle');
+    const messages = browserConsole.stop();
+    await Gatherer.stop();
+
+    const notFoundMessage = messages.find(
+      m => m.text.indexOf('Failed to load resource:') >= 0
+    );
+    expect(notFoundMessage.text).toEqual(
+      `Failed to load resource: the server responded with a status of 404 (Not Found)`
+    );
+    expect(notFoundMessage.type).toEqual('error');
+    expect(notFoundMessage.step).toEqual(currentStep);
+
+    const referenceError = messages.find(
+      m => m.text.indexOf('that is not defined') >= 0
+    );
+    expect(referenceError.error.stack).toContain(
+      `ReferenceError: that is not defined\n    at HTMLImageElement.onerror`
+    );
+    expect(referenceError.type).toEqual('error');
+    expect(referenceError.step).toEqual(currentStep);
+  });
+
+  it('should capture unhandled rejections', async () => {
+    const driver = await Gatherer.setupDriver({ wsEndpoint });
+    const browserConsole = new BrowserConsole(driver);
+    browserConsole.start();
+    browserConsole._currentStep = currentStep;
+    await driver.page.goto(server.TEST_PAGE);
+    await driver.page.setContent(
+      `<script>Promise.reject(new Error("Boom"))</script>`
+    );
+    await driver.page.waitForLoadState('networkidle');
+    const messages = browserConsole.stop();
+    await Gatherer.stop();
+
+    const unhandledError = messages.find(m => m.text.indexOf('Boom') >= 0);
+    expect(unhandledError.type).toEqual('error');
+    expect(unhandledError.error.stack).toContain('Error: Boom');
+    expect(unhandledError.step).toEqual(currentStep);
   });
 });
diff --git a/__tests__/reporters/__snapshots__/json.test.ts.snap b/__tests__/reporters/__snapshots__/json.test.ts.snap
index ed4f2c4..5086664 100644
--- a/__tests__/reporters/__snapshots__/json.test.ts.snap
+++ b/__tests__/reporters/__snapshots__/json.test.ts.snap
@@ -356,6 +356,7 @@ exports[`json reporter writes each step as NDJSON to the FD 1`] = `
 {\\"type\\":\\"step/filmstrips\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1},\\"root_fields\\":{\\"browser\\":{\\"relative_trace\\":{\\"start\\":{\\"us\\":392583998697}}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"index\\":0},\\"blob\\":\\"dummy\\",\\"blob_mime\\":\\"image/jpeg\\",\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"step/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1,\\"status\\":\\"succeeded\\",\\"duration\\":{\\"us\\":10000000}},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"source\\":\\"() => { }\\",\\"url\\":\\"dummy\\",\\"status\\":\\"succeeded\\"},\\"url\\":\\"dummy\\",\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"journey/network_info\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"root_fields\\":{\\"user_agent\\":{},\\"http\\":{\\"request\\":{\\"body\\":{\\"bytes\\":0,\\"content\\":\\"\\"}}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"browser\\":{},\\"is_navigation_request\\":true},\\"package_version\\":\\"0.0.1\\"}
+{\\"type\\":\\"journey/browserconsole\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"step-name\\",\\"index\\":0},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"text\\":\\"Boom\\",\\"type\\":\\"error\\"},\\"error\\":{\\"name\\":\\"Error\\",\\"message\\":\\"boom\\",\\"stack\\":\\"\\"},\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"journey/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\",\\"status\\":\\"succeeded\\"},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"start\\":0,\\"end\\":11,\\"status\\":\\"succeeded\\"},\\"package_version\\":\\"0.0.1\\"}
 "
 `;
diff --git a/__tests__/reporters/json.test.ts b/__tests__/reporters/json.test.ts
index ba49226..44b6261 100644
--- a/__tests__/reporters/json.test.ts
+++ b/__tests__/reporters/json.test.ts
@@ -103,6 +103,8 @@ describe('json reporter', () => {
   };
 
   it('writes each step as NDJSON to the FD', async () => {
+    const error = new Error('boom');
+    error.stack = '';
     runner.emit('journey:register', {
       journey: j1,
     });
@@ -173,6 +175,15 @@ describe('json reporter', () => {
           browser: {},
         } as any,
       ],
+      browserconsole: [
+        {
+          timestamp,
+          text: 'Boom',
+          type: 'error',
+          step: { name: 'step-name', index: 0 },
+          error,
+        },
+      ],
     });
     runner.emit('end', 'done');
     expect((await readAndCloseStream()).toString()).toMatchSnapshot();