<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2000/Atom">
  <title>Hardkoded</title>
  <subtitle>Dev notes from the maintainer of Puppeteer-Sharp</subtitle>
  <link href="https://www.hardkoded.com/atom.xml" rel="self"/>
  <link href="https://www.hardkoded.com/"/>
  <updated>2026-09-05T17:49:12+00:00</updated>
  <id>https://www.hardkoded.com/</id>
  <author>
    <name>Darío Kondratiuk</name>
    <email>dariokondratiuk@gmail.com</email>
  </author>
  
  <entry>
    <title>Stream Deck as an Agentic Productivity Tool</title>
    <link href="https://www.hardkoded.com/blog/stream-deck-as-an-agentic-productivity-tool"/>
    <updated>2026-09-04T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/stream-deck-as-an-agentic-productivity-tool</id>
    <content type="html">&lt;p&gt;In &lt;a href=&quot;/blog/getting-out-of-worktree-hell&quot;&gt;Getting Out of Worktree Hell&lt;/a&gt; I wrote about stable worktrees: a fixed set of six, wt1 through wt6, each tied to its own &lt;a href=&quot;https://cmux.com&quot;&gt;cmux&lt;/a&gt; session. Same names everywhere — terminal, Chrome tab group, piece of work.&lt;/p&gt;

&lt;p&gt;That fixed the chaos. It did not fix the switching. When Claude is running across those workspaces and I’m in a browser reviewing a PR, jumping back still means hunting the right tab, the right session, the right place where something is waiting for me.&lt;/p&gt;

&lt;h1 id=&quot;not-just-for-streamers&quot;&gt;Not just for streamers&lt;/h1&gt;

&lt;p&gt;Elgato’s Stream Deck shows up in every streamer desk photo. Lights, scenes, mute. Fine. But a grid of programmable keys with live status is also a pretty good surface for an agentic workflow.&lt;/p&gt;

&lt;p&gt;You are not one person at one terminal anymore. You are coordinating several agents, several worktrees, several “please pick one” prompts. A physical deck is a control panel for that — status at a glance, one tap to jump, no ⌘-Tab spelunking.&lt;/p&gt;

&lt;h1 id=&quot;worktrees-on-the-deck&quot;&gt;Worktrees on the deck&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/gonzaloserrano&quot;&gt;Gonzalo Serrano&lt;/a&gt; built &lt;a href=&quot;https://github.com/gonzaloserrano/streamdeck-cmux&quot;&gt;streamdeck-cmux&lt;/a&gt;, and it maps straight onto the stable-worktree setup.&lt;/p&gt;

&lt;p&gt;Each key is a cmux workspace. Background color matches the sidebar. A lighter key is the one you are on. An orange bar means it needs input. Pink means Claude is running. Progress and working directory show up on the key itself.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kblok/kblok.github.io/master/img/stream-deck-as-an-agentic-productivity-tool/streamdeck-cmux.png&quot; style=&quot;display: block; margin: auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Press a key and you land on that workspace — the same wt1–wt6 I already use as a WIP limit, now one tap away from whatever I was reading in the browser.&lt;/p&gt;

&lt;h1 id=&quot;answering-claude-from-the-keys&quot;&gt;Answering Claude from the keys&lt;/h1&gt;

&lt;p&gt;Claude Code asks a lot of multiple-choice questions. Leave the keyboard, find the terminal, read the options, type a number. Fine once. Annoying when six worktrees are asking at once.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href=&quot;https://github.com/hardkoded/streamdeck-claude-answer&quot;&gt;streamdeck-claude-answer&lt;/a&gt;. When Claude asks, the question lands on the deck. Top-left tells you which directory is asking. The numbered keys are the options. Press one, the answer goes back, and the deck returns to whatever profile you were on.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kblok/kblok.github.io/master/img/stream-deck-as-an-agentic-productivity-tool/deck-question.png&quot; style=&quot;display: block; margin: auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;No network daemon. Claude writes a question file, the plugin watches it, you press a key, an answer file goes back. That is the whole loop.&lt;/p&gt;

&lt;h1 id=&quot;will-it-stick&quot;&gt;Will it stick?&lt;/h1&gt;

&lt;p&gt;I don’t know how well this will integrate into my daily workflow. Buttons are fun for a week and then they collect dust. But the problem is real — too many agents, too many waiting prompts — and a deck that shows status and takes answers feels worth trying.&lt;/p&gt;

&lt;p&gt;I’ll give it a shot.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>The state of unit testing in the AI era</title>
    <link href="https://www.hardkoded.com/blog/the-state-of-unit-testing-in-the-ai-era"/>
    <updated>2026-08-14T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/the-state-of-unit-testing-in-the-ai-era</id>
    <content type="html">&lt;p&gt;The title of this post looks like deep research of unit testing and AI, but it’s more a set of random thoughts.&lt;/p&gt;

&lt;p&gt;Unit testing used to be an important part of our code review. That was the first thing I would look at on a PR. Tests were supposed to show the reviewer how the code works, and then you would go to the implementation.&lt;/p&gt;

&lt;p&gt;Remember TDD?&lt;/p&gt;

&lt;p&gt;We also fought for test coverage. “Hey, this line doesn’t have coverage?”&lt;/p&gt;

&lt;p&gt;Now we take unit tests and test coverage for granted. The AI will make sure there is a unit test for every single line of code.&lt;/p&gt;

&lt;p&gt;And the main problem is that those tests are what I call self-reinforcement tests. The AI validates what the AI did, not what the AI was supposed to do.&lt;/p&gt;

&lt;p&gt;And it creates lots of tests. LOTS.&lt;/p&gt;

&lt;p&gt;I was talking with a colleague about this a few days ago and I found something pretty sad. We ended up just marking the test files as “viewed” in GitHub.&lt;/p&gt;

&lt;p&gt;What used to be the entry point for reviewing is now noise.&lt;/p&gt;

&lt;p&gt;I created some skills and rules to fight that. The one that stuck: split the tests.&lt;/p&gt;

&lt;p&gt;Spec tests describe what a feature does. A handful per file. They read like documentation. Those are the ones you open first on a PR.&lt;/p&gt;

&lt;p&gt;Unit tests pin down individual functions and edge cases. As many as you need. They can live in a different file so they don’t bury the story.&lt;/p&gt;

&lt;p&gt;Same runner, same CI. The split is for humans.&lt;/p&gt;

&lt;p&gt;It helps. A bit. You can find the tests that still mean something.&lt;/p&gt;

&lt;p&gt;But it looks like we need to make an effort to go back and give unit tests the place they deserve.&lt;/p&gt;

&lt;p&gt;Or is it that we don’t need them anymore?&lt;/p&gt;

&lt;p&gt;But there are two things I will never skip. E2E tests for the UI. And it might sound old-fashioned, but I will ALWAYS do a manual test on user-facing features.&lt;/p&gt;

&lt;p&gt;This sits next to how I work with agents day to day: &lt;a href=&quot;/blog/getting-out-of-worktree-hell&quot;&gt;getting out of worktree hell&lt;/a&gt; and &lt;a href=&quot;/blog/pr-memory&quot;&gt;pr-memory&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;I don’t have a clean answer. I know I don’t want to mark 40 test files as viewed and call it a review. If the tests don’t tell me what the code is supposed to do, they’re just the AI talking to itself.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>New .NET Library in Town. Let&apos;s Welcome ReactiveExtensions-Sharp</title>
    <link href="https://www.hardkoded.com/blog/reactive-extensions-sharp"/>
    <updated>2026-08-09T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/reactive-extensions-sharp</id>
    <content type="html">&lt;p&gt;Puppeteer-Sharp has one simple goal: to be a port of Puppeteer, as close as possible.&lt;/p&gt;

&lt;p&gt;Close in the API. Close in the .NET flavor, because a C# developer should feel at home. But also, and this is the part nobody sees, close in the internals. When the internals match, porting an upstream fix is a mechanical job instead of a research project.&lt;/p&gt;

&lt;p&gt;And that’s where things get interesting, because Puppeteer uses &lt;a href=&quot;https://rxjs.dev/&quot;&gt;rxjs&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;the-rxjs-inside-puppeteer&quot;&gt;The rxjs inside Puppeteer&lt;/h1&gt;

&lt;p&gt;Some of it is small and lovely. This is Puppeteer waiting for the fonts to be ready before printing a PDF:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;firstValueFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mainFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isolatedRealm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fonts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;raceWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Read it once more. “Take this promise, turn it into a stream, and race it against a timer.” One expression. No token source, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Task.WhenAny&lt;/code&gt;, no cleanup.&lt;/p&gt;

&lt;p&gt;And then there is stuff like this, which is how the BiDi implementation waits for a navigation:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;firstValueFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;combineLatest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;race&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;browsingContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;browsingContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;historyUpdated&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;switchMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForLoad$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;delayWhen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;frames&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;combineLatest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;frames&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;raceWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
              &lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;failed&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
              &lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;aborted&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;switchMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requestFinished$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Navigation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requestFinished$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                      &lt;span class=&quot;nx&quot;&gt;raceWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
                      &lt;span class=&quot;nx&quot;&gt;raceWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fromEmitterEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;redirect&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                      &lt;span class=&quot;nx&quot;&gt;switchMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requestFinished$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                      &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requestFinished$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;waitForNetworkIdle$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(([&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;raceWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fromAbortSignal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detached$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TargetCloseError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Frame detached.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I won’t pretend I understood that on the first read. Or the second. But once you do, &lt;strong&gt;it says exactly what a navigation is&lt;/strong&gt;: race a navigation event against a history update, wait for the load, wait for the child frames, wait for the request chain to settle, and race the whole thing against a timeout, an abort signal, and the frame being detached.&lt;/p&gt;

&lt;p&gt;Every “and also this can happen” is one more line, not one more branch.&lt;/p&gt;

&lt;h1 id=&quot;doing-that-in-plain-c-is-not-impossible&quot;&gt;Doing that in plain C# is not impossible&lt;/h1&gt;

&lt;p&gt;It’s just very hard to keep up with.&lt;/p&gt;

&lt;p&gt;Puppeteer-Sharp’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Locator&lt;/code&gt; retry loop is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while (true)&lt;/code&gt; with five catch clauses, one of them holding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;try&lt;/code&gt; inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;catch&lt;/code&gt;, plus a comment explaining that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;try&lt;/code&gt; inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;catch&lt;/code&gt;, because otherwise nobody would believe it was on purpose. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForNetworkIdleAsync&lt;/code&gt; is the same story with different pieces: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;System.Timers.Timer&lt;/code&gt;, four event handlers, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cleanup()&lt;/code&gt; local function called from three different places, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TaskCompletionSource&lt;/code&gt; to tie it all together.&lt;/p&gt;

&lt;p&gt;That code works. I’m not ashamed of it. But it is bespoke, and that’s the real cost. When upstream changes one line of an rxjs pipeline, I can’t just port that line. I have to read the new semantics and figure out where they land inside my own imperative version. That’s not porting. That’s re-deriving, every single time.&lt;/p&gt;

&lt;h1 id=&quot;but-net-already-has-rx&quot;&gt;“But .NET already has Rx”&lt;/h1&gt;

&lt;p&gt;I know what you might be thinking, and it’s true. .NET has &lt;a href=&quot;https://github.com/dotnet/reactive&quot;&gt;Rx.NET&lt;/a&gt;. It’s mature, it’s first class, and it was there before rxjs.&lt;/p&gt;

&lt;p&gt;But Rx.NET and rxjs are not the same library with different syntax. They are two implementations of the same idea that evolved separately for more than a decade. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Select&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Where&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;raceWith&lt;/code&gt; is sort of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Amb&lt;/code&gt;. “Retry with a delay between attempts” is not one call. And where the names do match, the defaults sometimes don’t.&lt;/p&gt;

&lt;p&gt;So porting an rxjs pipeline to Rx.NET is a translation, operator by operator, hoping every edge case lines up.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I didn’t want a translation. I wanted the same code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;so-now-we-have-reactiveextensions-sharp&quot;&gt;So now we have ReactiveExtensions-Sharp&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/hardkoded/ReactiveExtensions-Sharp&quot;&gt;ReactiveExtensions-Sharp&lt;/a&gt; is a port of RxJS to .NET. Not “another Rx library”. A port. Same operator names, same semantics, same edge cases, checked test by test against RxJS’s own spec suite at tag &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7.8.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForNetworkIdleAsync&lt;/code&gt; now looks like this:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idleReached&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_inFlightRequestCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;AsObservable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;concurrency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;DistinctUntilChanged&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SwitchMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idle&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TimeSpan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FromMilliseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Never&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idleReached&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;RaceWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CloseSignal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;())&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;RaceWithSignalAndTimer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TimeSpan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FromMilliseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cancellationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ConfigureAwait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s the whole method. The timer is gone. The four event handlers are gone. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cleanup()&lt;/code&gt; is gone, because unsubscribing &lt;em&gt;is&lt;/em&gt; the cleanup.&lt;/p&gt;

&lt;p&gt;And those five catch clauses in the locator become this:&lt;/p&gt;

&lt;div class=&quot;language-csharp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Observable&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Defer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;From&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linkedToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;RetryAndRaceWithSignalAndTimer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;TimeSpan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FromMilliseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Timeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;causeFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;retryDelay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TimeSpan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;FromMilliseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RetryDelay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cancellationToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ConfigureAwait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RetryAndRaceWithSignalAndTimer&lt;/code&gt; is not something I invented. It’s the C# name for the combinator Puppeteer defines for itself in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;locators.ts&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pipe(retry({delay}), raceWith(fromAbortSignal(...), timeout(...)))&lt;/code&gt;. Same three operators, same order, same behavior.&lt;/p&gt;

&lt;p&gt;There is an &lt;a href=&quot;https://github.com/hardkoded/puppeteer-sharp/pull/3520&quot;&gt;open PR&lt;/a&gt; doing this across &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Locator&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForNetworkIdle&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForRequest&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForResponse&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForFrame&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitForTarget&lt;/code&gt;. 232 lines added, 502 lines deleted.&lt;/p&gt;

&lt;h2 id=&quot;whats-in-the-box&quot;&gt;What’s in the box&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;100+ operators and creation functions. The whole RxJS 7.8.2 surface, not a curated subset. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;groupBy&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expand&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;partition&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;using&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bindCallback&lt;/code&gt;, the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window*&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buffer*&lt;/code&gt; families, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;share&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shareReplay&lt;/code&gt; with the complete reset config.&lt;/li&gt;
  &lt;li&gt;800+ tests, ported from RxJS’s own spec files. Not written from the docs. The same cases RxJS uses to check itself.&lt;/li&gt;
  &lt;li&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VirtualTimeScheduler&lt;/code&gt; and a marble-style &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestScheduler&lt;/code&gt;, so you can assert that a debounce or a retry-with-backoff behaves correctly across virtual time, without waiting a single real millisecond.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netstandard2.0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;net8.0&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;net10.0&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;On &lt;a href=&quot;https://www.nuget.org/packages/ReactiveExtensionsSharp/&quot;&gt;NuGet&lt;/a&gt; as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReactiveExtensionsSharp&lt;/code&gt;, with &lt;a href=&quot;https://hardkoded.github.io/ReactiveExtensions-Sharp/&quot;&gt;API docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;but-now-we-have-agents&quot;&gt;But now we have agents&lt;/h1&gt;

&lt;p&gt;This project sat on my “someday” list for years. Porting a full operator surface, test by test, is a lot of typing and not much thinking, and I’m one person with a day job.&lt;/p&gt;

&lt;p&gt;And that turned out to be exactly the shape of work agents are good at. “Port this one operator, verify it against this one upstream spec file” is small, self-contained, and independently checkable. So most milestones ran as several agent sessions at the same time, each one in its own git worktree, each one landing its own branch.&lt;/p&gt;

&lt;p&gt;Was it magic?
.
.
.
.
No.&lt;/p&gt;

&lt;p&gt;The most interesting bug in this repo is about disposal. RxJS has a specific trick that makes “stop listening” cascade all the way up a chain of operators. The obvious C# translation works fine for async sources and silently fails for synchronous ones, because the whole emission loop finishes before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Subscribe()&lt;/code&gt; even returns, so the “stop” signal arrives too late to attach anywhere.&lt;/p&gt;

&lt;p&gt;That bug was found five separate times, by five sessions that knew nothing about each other, each one patching its own operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five independent rediscoveries is not five mistakes. It’s a sign the problem is structural.&lt;/strong&gt; It only got fixed properly, once, at the architecture level, when somebody looked at all five reports together.&lt;/p&gt;

&lt;p&gt;Later, an audit compared every ported test file against its real upstream spec. I expected it to find thin coverage. It found four genuine bugs, all of them while &lt;em&gt;writing&lt;/em&gt; the missing tests: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;concat&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt; had that same synchronous disposal bug in a corner nobody had checked, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;combineLatest&lt;/code&gt; completed too early when a source finished without ever emitting, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;concatMap&lt;/code&gt; could start the next inner observable before the previous cleanup had finished, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;takeUntil&lt;/code&gt; had no error handler on its notifier at all, so an errored notifier took the whole process down.&lt;/p&gt;

&lt;p&gt;Agents wrote most of the code. The bugs were found by reading, comparing and asking questions. That part is still ours.&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;You probably noticed I keep saying “we”. That’s not a writing tic. Everything I do is community based. ReactiveExtensions-Sharp is not mine. It’s ours.&lt;/p&gt;

&lt;p&gt;It’s at 0.3.0. It’s young. The surface is complete, but the mileage is low, and low mileage is exactly what bug reports are for.&lt;/p&gt;

&lt;p&gt;So bring new ideas, send PRs, fork it, break it! 😉&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Claude Helped Me Be Lazy, Again</title>
    <link href="https://www.hardkoded.com/blog/claude-helped-me-be-lazy-again"/>
    <updated>2026-07-31T21:33:03+00:00</updated>
    <id>https://www.hardkoded.com/blog/claude-helped-me-be-lazy-again</id>
    <content type="html">&lt;p&gt;I found out that my &lt;a href=&quot;https://github.com/kblok&quot;&gt;GitHub profile&lt;/a&gt; was quite old. And broken. Some links didn’t work anymore, and some things were just there because I put them there years ago and then forgot about them.&lt;/p&gt;

&lt;p&gt;So I did what we do now: I asked Claude to update it.&lt;/p&gt;

&lt;p&gt;It found a few things to fix, and then it said something like: “Hey, you have a list of your blog posts here and they are very old. Do you want me to update them?”&lt;/p&gt;

&lt;p&gt;Yeah, sure. Why not?&lt;/p&gt;

&lt;p&gt;Then I saw it fetching &lt;a href=&quot;https://github.com/gautamkrishnar&quot;&gt;gautamkrishnar&lt;/a&gt;’s GitHub profile.&lt;/p&gt;

&lt;p&gt;Ok, this is wrong.&lt;/p&gt;

&lt;p&gt;It turns out it wasn’t wrong at all. &lt;a href=&quot;https://github.com/gautamkrishnar&quot;&gt;Gautam Krishna&lt;/a&gt; built &lt;a href=&quot;https://github.com/gautamkrishnar/blog-post-workflow&quot;&gt;blog-post-workflow&lt;/a&gt;, a GitHub Action that reads your blog feed and keeps the posts in your README updated.&lt;/p&gt;

&lt;p&gt;I never would have taken the time to look for a tool to do that for me. Not because it is hard, but because it is one of those small things that are always nice to have and never important enough to spend time on.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Claude didn’t just update my profile. It found the automation I was too lazy to look for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now my profile can keep itself updated when I publish something here.&lt;/p&gt;

&lt;p&gt;Claude helped my laziness, one more time.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Getting Out of Worktree Hell</title>
    <link href="https://www.hardkoded.com/blog/getting-out-of-worktree-hell"/>
    <updated>2026-07-20T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/getting-out-of-worktree-hell</id>
    <content type="html">&lt;p&gt;Git worktrees aren’t new. They shipped in &lt;a href=&quot;https://github.blog/open-source/git/git-2-5-including-multiple-worktrees-and-triangular-workflows/&quot;&gt;Git 2.5&lt;/a&gt;, back in July 2015. And for years, almost nobody used them. Checking out a second branch into a second folder was a nice trick for the rare case, not something you reached for every day.&lt;/p&gt;

&lt;p&gt;Then AI coding agents showed up, and worktrees became the obvious answer to a new question: how do you run more than one agent on the same repo at the same time? You can’t have two agents fighting over the same branch and the same files. Give each one its own worktree, and they stop stepping on each other.&lt;/p&gt;

&lt;p&gt;Claude Code leans into this hard. Ask it to start a task and it will happily create a worktree for you, attach it to a branch, and clean it up once that branch is gone. It’s smooth. It’s also how you end up in worktree hell.&lt;/p&gt;

&lt;h1 id=&quot;how-you-end-up-here&quot;&gt;How you end up here&lt;/h1&gt;

&lt;p&gt;One worktree per task sounds great, until you have a dozen of them open. Each one is a PR in some state: waiting on CI, waiting on review, waiting on you to remember what you were even doing in there. I built &lt;a href=&quot;/blog/pr-memory&quot;&gt;pr-memory&lt;/a&gt; to fix the “wait, what was I doing” half of that problem. But it doesn’t fix the other half: too many folders, too many terminals, too much to hold in your head at once.&lt;/p&gt;

&lt;p&gt;And then, one day, you get a “disk full” alert on a machine that should have plenty of space. You dig in, and it turns out half your disk is dead worktrees: branches that merged weeks ago, folders nobody removed, build output sitting there because Claude Code created the worktree but nothing ever told it to clean up after the PR closed.&lt;/p&gt;

&lt;h1 id=&quot;it-depends-on-the-repo&quot;&gt;It depends on the repo&lt;/h1&gt;

&lt;p&gt;None of this is a problem on a small, single-repo project. &lt;a href=&quot;https://github.com/hardkoded/puppeteer-sharp&quot;&gt;puppeteer-sharp&lt;/a&gt; is one repo with a light build; worktrees come and go and you’d never notice. But once you’re working across several related repos, or a big repo with a heavy build — an Electron app, say, where every checkout drags a chunky &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; and a pile of build artifacts behind it — an unmanaged stack of worktrees turns from an annoyance into a real problem, on disk and in your head.&lt;/p&gt;

&lt;h1 id=&quot;stable-worktrees&quot;&gt;Stable worktrees&lt;/h1&gt;

&lt;p&gt;For my “enterprise” work, I stopped letting worktrees multiply and switched to what I call stable worktrees: a fixed set that’s always there, reused instead of recreated.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kblok/kblok.github.io/master/img/getting-out-of-worktree-hell/stable-worktrees.png&quot; style=&quot;display: block; margin: auto;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I keep six of them, wt1 through wt6, each one tied to its own &lt;a href=&quot;https://cmux.com&quot;&gt;cmux&lt;/a&gt; session, and each cmux session living in its own Chrome tab group with the matching name. Same six names everywhere, so I always know which terminal, which browser tabs, and which piece of work go together.&lt;/p&gt;

&lt;p&gt;There’s also a seventh, wt0. It isn’t a real worktree — it’s just a cmux tab for the stuff that doesn’t belong to any single task: loops, PR checks, crons, the background chores that don’t need a workspace of their own.&lt;/p&gt;

&lt;h1 id=&quot;the-limit-is-the-point&quot;&gt;The limit is the point&lt;/h1&gt;

&lt;p&gt;Six sounds restrictive, and it is. That’s what makes it work. When all six are busy, I can’t just spin up a seventh — I have to finish something, ship it, or park it, before I start anything new. It turns the worktrees into a kanban board with a hard WIP limit, and it’s done more for shipping than any productivity tip I’ve tried: fewer half-finished branches, more PRs actually landing.&lt;/p&gt;

&lt;p&gt;The other mess those agents create — a wall of tests nobody reads — is &lt;a href=&quot;/blog/the-state-of-unit-testing-in-the-ai-era&quot;&gt;a different post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Meet pr-memory, a Way to Put a Claude Code Session on Hold</title>
    <link href="https://www.hardkoded.com/blog/pr-memory"/>
    <updated>2026-07-13T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/pr-memory</id>
    <content type="html">&lt;p&gt;If you work on more than one thing at a time with Claude Code, you know the problem. You’ve got a worktree for one feature, a terminal for a bug fix, another tab for a PR that’s waiting on review. Each session has built up real context — what was tried, what failed, why a decision was made — and none of that is written down anywhere. It only exists in that one running session.&lt;/p&gt;

&lt;p&gt;Claude Code already lets you name a session and resume it later, but in practice that’s tedious. You have to remember the name, remember which terminal, remember which of the five things you were juggling that session belongs to. When you’re deep in several worktrees at once, it’s easy to just lose track.&lt;/p&gt;

&lt;p&gt;What I actually wanted was simpler: work on a PR, step away from it, and free up the terminal for something else — without losing everything that session knew.&lt;/p&gt;

&lt;p&gt;So I built the &lt;a href=&quot;https://github.com/hardkoded/hardkoded-skills/tree/main/plugins/hardkoded/skills/pr-memory&quot;&gt;pr-memory&lt;/a&gt; skill.&lt;/p&gt;

&lt;h1 id=&quot;what-it-is&quot;&gt;What it is&lt;/h1&gt;

&lt;p&gt;pr-memory keys session memory off the thing you actually care about: the pull request.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-memory save&lt;/code&gt; — snapshots the current session as a handoff-style document: the worktree, the branch in every repo you touched, and every related PR across repos.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-memory load &amp;lt;pr&amp;gt;&lt;/code&gt; — finds that document by any of those PR numbers and re-hydrates the session from it.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-memory list&lt;/code&gt; — see everything you’ve got saved.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pr-memory gc&lt;/code&gt; — prune memories whose PRs have all merged (this also runs automatically in the background).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Say you’re working on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myrepo#5&lt;/code&gt;. You need the terminal for something more urgent, but you’re not done with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#5&lt;/code&gt; yet. Instead of leaving the session running or trying to remember a session name, you just say:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Save this session”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and pr-memory writes a handoff doc — what was built, current state, open items, key learnings — indexed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#5&lt;/code&gt;. The terminal is now free.&lt;/p&gt;

&lt;p&gt;Later, review feedback comes in, or you just want to pick it back up:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Load pr-memory for #5”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and the session comes back: same context, same open items, with an offer to check out the saved branch if you’re not already on it.&lt;/p&gt;

&lt;p&gt;It started as a personal skill tied to one multi-repo workspace I use at work, but I generalized it to work standalone with zero config in a plain single-repo checkout, while still scaling up to a multi-repo workspace if you have one.&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final Words&lt;/h1&gt;

&lt;p&gt;It’s a small thing, but it’s already changed how I juggle multiple PRs — I stop hoarding terminal tabs “just in case” and just trust that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;save&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;load&lt;/code&gt; will bring the context back when I need it.&lt;/p&gt;

&lt;p&gt;It’s part of the &lt;a href=&quot;https://github.com/hardkoded/hardkoded-skills/tree/main/plugins/hardkoded/skills/pr-memory&quot;&gt;hardkoded-skills&lt;/a&gt; plugin, open source as always. Give it a try and let me know what breaks.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Meet Cronito, a Friendlier Cron for Claude Code</title>
    <link href="https://www.hardkoded.com/blog/cronito"/>
    <updated>2026-07-07T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/cronito</id>
    <content type="html">&lt;p&gt;Everybody is talking about loops these days. Loop this, loop that, wrap your agent in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt; and let it check on things every few minutes. And sure, that works — until you close the laptop, or the session times out, or you just forget the terminal tab was even open. Your loop needed a live session to stay alive, and the moment that session is gone, so is your loop.&lt;/p&gt;

&lt;p&gt;I hit this constantly with Claude Code: check on a PR every few minutes, run a nightly cleanup, poll a flaky pipeline until it turns green. None of that should depend on me keeping a terminal open. What I actually wanted was a real, boring, system-level scheduler — the kind that keeps running whether or not I’m looking at it.&lt;/p&gt;

&lt;p&gt;So, cron. Except raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crontab -e&lt;/code&gt; doesn’t tell you what ran, when it last ran, or why it failed. You end up with five undocumented lines nobody remembers writing, including you, three weeks later. And when the thing you’re scheduling involves Claude Code itself, there’s an extra headache: cron and launchd run outside your logged-in session, so anything that assumes you’re “logged in” quietly breaks, and now you’re debugging authentication instead of your actual task.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href=&quot;https://github.com/hardkoded/cronito&quot;&gt;cronito&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;what-it-is&quot;&gt;What it is&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;One unified, friendly interface for scheduled tasks, backed by a single system cron entry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of hand-editing crontab syntax, you get:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito add&lt;/code&gt; — create a task interactively: name, command, working directory, schedule&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito list&lt;/code&gt; — see everything at a glance, with status&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito edit&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito remove&lt;/code&gt; — change or delete a task by id&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito enable&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito disable&lt;/code&gt; — toggle a task without deleting it&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito logs&lt;/code&gt; — execution history, so you actually know what happened&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito status&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito dashboard&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronito health&lt;/code&gt; — a real picture of your scheduler, not a guess&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It ships as a single dependency-free bundle, so a global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install -g cronito&lt;/code&gt; (or running the bundle directly) is all it takes. One system cron entry drives everything; cronito routes to the right task under the hood.&lt;/p&gt;

&lt;p&gt;A quick example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ cronito add
? Task name: nightly-cleanup
? Command: ./scripts/cleanup.sh
? Working directory: ~/projects/myapp
? Schedule (cron syntax): 0 3 * * *
✔ Task &quot;nightly-cleanup&quot; added

$ cronito list
ID  NAME              SCHEDULE     STATUS   LAST RUN
1   nightly-cleanup   0 3 * * *    enabled  —
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;but-i-mostly-use-it-from-inside-claude-code&quot;&gt;But I mostly use it from inside Claude Code&lt;/h1&gt;

&lt;p&gt;Here’s the part I actually wanted: I don’t want to leave my Claude Code session just to schedule something. So I also built a &lt;a href=&quot;https://github.com/hardkoded/hardkoded-skills/tree/main/plugins/hardkoded/skills/cronito&quot;&gt;cronito skill&lt;/a&gt; for Claude Code.&lt;/p&gt;

&lt;p&gt;It vendors the same dependency-free bundle, so there’s no separate install step — the skill just works. Inside a session, you can type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/cronito&lt;/code&gt;, or ask in plain language:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Check on this deploy every 5 minutes until it’s done”&lt;/p&gt;

  &lt;p&gt;“Schedule a task that runs my test suite every morning at 8am”&lt;/p&gt;

  &lt;p&gt;“List my scheduled tasks”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and Claude drives cronito directly — same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;logs&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dashboard&lt;/code&gt; commands, just without you touching a terminal.&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final Words&lt;/h1&gt;

&lt;p&gt;Both repos are open source, and I’d genuinely like feedback if you try them:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/hardkoded/cronito&quot;&gt;cronito&lt;/a&gt; — the CLI&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/hardkoded/hardkoded-skills/tree/main/plugins/hardkoded/skills/cronito&quot;&gt;cronito skill&lt;/a&gt; — the Claude Code plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve been duct-taping cron jobs together too, give it a try and let me know what breaks.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Back to Sharing in a Post-Blog Era</title>
    <link href="https://www.hardkoded.com/blog/back-to-sharing-in-a-post-blog-era"/>
    <updated>2026-07-03T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/back-to-sharing-in-a-post-blog-era</id>
    <content type="html">&lt;p&gt;The last time I shared something on this blog was in 2023. Almost three years!! No, I didn’t retire (yet). I was just working. A lot.&lt;/p&gt;

&lt;p&gt;Let’s be honest: blogs are dead. I don’t read blogs anymore either. All the good content today lives in X posts, LinkedIn articles, and, well, in a chat with Claude.&lt;/p&gt;

&lt;p&gt;So coming back to writing here is hard. You lose the exercise, and once you lose it, sitting down to write again feels like starting from zero. But here I am, back in this post-blog era. And, not by coincidence, in the middle of an AI era too.&lt;/p&gt;

&lt;p&gt;I use AI pretty much all the time now, and I’m not the only one. A lot of folks are doing the same thing, and sharing it.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I want to share things that actually help the community, not just chase engagement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s what I’ve been trying to do for the last 8 years, and the format changing doesn’t change that.&lt;/p&gt;

&lt;p&gt;A few things already in motion:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Puppeteer-Sharp is now fully automated with AI. You’ll hear more about that soon.&lt;/li&gt;
  &lt;li&gt;A fully native Playwright-Sharp version, coded end-to-end with AI, is coming.&lt;/li&gt;
  &lt;li&gt;Soon I’ll start sharing some of the AI skills I use every day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a first example: if you’re reading this post on X or LinkedIn, it got there through one of those skills, &lt;a href=&quot;https://github.com/kblok/kblok.github.io/tree/master/.claude/skills/promote-post&quot;&gt;promote-post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I hope you can get more of my posts soon :)&lt;br /&gt;
Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Puppeteer-Sharp 2023 Recap</title>
    <link href="https://www.hardkoded.com/blog/puppeteer-sharp-2023-recap"/>
    <updated>2023-12-31T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/puppeteer-sharp-2023-recap</id>
    <content type="html">&lt;p&gt;It has been a while since I wrote those &lt;a href=&quot;https://www.hardkoded.com/blog/whats-new-puppeteer-sharp-april-2022&quot;&gt;Puppeteer-Sharp blog posts&lt;/a&gt; (maybe it’s time to do it again?). However, I thought it would be nice to recap all the progress made in 2023.&lt;/p&gt;

&lt;p&gt;It can be quite challenging to keep Puppeteer-Sharp up-to-date. On one side I was implementing great features at &lt;a href=&quot;https://www.mabl.com/&quot;&gt;mabl&lt;/a&gt;, that took most of my time. Additionally, I love using &lt;a href=&quot;https://playwright.dev/&quot;&gt;playwright&lt;/a&gt;! In my opinion, mabl is the leading low-code testing platform in the market, while playwright is the best end-to-end testing library.&lt;/p&gt;

&lt;p&gt;So, how about Puppeteer-Sharp?&lt;/p&gt;

&lt;p&gt;I hope that Puppeteer-Sharp can become the best multi-purpose browser automation library, the most complete &lt;a href=&quot;https://chromedevtools.github.io/devtools-protocol/&quot;&gt;CDP&lt;/a&gt; API in .net, and maybe, why not? the best/first &lt;a href=&quot;https://developer.chrome.com/docs/web-platform/best-practices/webdriver-bidi&quot;&gt;Webdriver BiDi&lt;/a&gt; library for .net?&lt;/p&gt;

&lt;p&gt;In 2023, Puppeteer-Sharp achieved over 9 million downloads and received sponsorship from the &lt;a href=&quot;https://github.com/aws/dotnet-foss?tab=readme-ov-file&quot;&gt;.NET on AWS Open Source Software Fund&lt;/a&gt;. These two acknowledgments were fuel for me.&lt;/p&gt;

&lt;p&gt;My main goal in 2023 was to close the gap with &lt;a href=&quot;https://github.com/puppeteer/puppeteer&quot;&gt;puppeteer&lt;/a&gt;. I was able to launch 19 versions! I didn’t expect that number to be that big!&lt;br /&gt;
Let me share with you two of the best features shipped this year.&lt;/p&gt;

&lt;h2 id=&quot;query-handlers&quot;&gt;Query handlers&lt;/h2&gt;

&lt;p&gt;You can query elements by text:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;SetContentAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;section&amp;gt;test&amp;lt;/section&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testEl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;QuerySelectorAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text/test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or even by Aria roles:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WaitForSelectorAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;aria/[role=\&quot;button\&quot;]&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Event XPaths are also implemented using a query handler:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;els&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;QuerySelectorAllAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;xpath///img&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;chrome-for-testing&quot;&gt;Chrome for testing&lt;/h2&gt;

&lt;p&gt;How we download browsers also had a significant change. You can now download &lt;a href=&quot;https://developer.chrome.com/blog/chrome-for-testing&quot;&gt;Chrome for testing&lt;/a&gt; instead of chromium. Keeping the browser up-to-date became a high-priority task.&lt;/p&gt;

&lt;p&gt;You can look at many other new features on the &lt;a href=&quot;https://github.com/hardkoded/puppeteer-sharp/releases?page=1&quot;&gt;release page&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;about-2024&quot;&gt;About 2024.&lt;/h1&gt;

&lt;p&gt;Closing the feature gap will still be my top priority. &lt;strong&gt;But&lt;/strong&gt;, the rumors about puppeteer moving to &lt;a href=&quot;https://developer.chrome.com/blog/puppeteer-webdriver-bidi-2023&quot;&gt;WebDriver Bidi&lt;/a&gt; sounds… exciting. Making Puppeteer-Sharp a great WebDriver BiDi API sounds promising.&lt;br /&gt;
On the personal side, I think this website need some refresh :)&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;What are you expecting from Puppeteer-Sharp in 2024? Should I write more? Should I share more examples? Let me know.&lt;/p&gt;

&lt;p&gt;And remember, Puppeteer-Sharp is a library BY the community FOR the community. We build this library together. The issues tab in GitHub is open for all of you to share your ideas and thoughts. You can also &lt;a href=&quot;https://github.com/sponsors/hardkoded&quot;&gt;sponsor the project&lt;/a&gt;. Even a single dollar would help me know you care about this project.&lt;/p&gt;

&lt;p&gt;Remember to follow me on X &lt;a href=&quot;https://twitter.com/hardkoded&quot;&gt;@hardkoded&lt;/a&gt; for more updates.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>What&apos;s new in Puppeteer-Sharp - April 2022</title>
    <link href="https://www.hardkoded.com/blog/whats-new-puppeteer-sharp-april-2022"/>
    <updated>2022-04-18T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/whats-new-puppeteer-sharp-april-2022</id>
    <content type="html">&lt;p&gt;I know, I know, I should be writing these posts more frequently, but I’m having so much fun at &lt;a href=&quot;https://www.mabl.com/join-the-team&quot;&gt;my new gig&lt;/a&gt;, and it’s not easy to keep up the pace.&lt;br /&gt;
It’s hard to believe it’s been a year! since my last blog post, but here we are in Puppeteer-Sharp 7!&lt;br /&gt;
Let me walk you through all the new features released since my &lt;a href=&quot;http://www.hardkoded.com/blog/puppeteer-sharp-3-is-here&quot;&gt;last blog post&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;chromium-100&quot;&gt;Chromium 100&lt;/h2&gt;

&lt;p&gt;The good news is that we now support Chrome/Chromium 100! But maybe not that good news is that Chromium 100 is now the minimum requirement due to many changes in how Chromium sends requests and responses data.&lt;/p&gt;

&lt;h2 id=&quot;keeping-the-fox-alive&quot;&gt;Keeping the fox alive&lt;/h2&gt;

&lt;p&gt;We’ve been doing some work so you can use most puppeteer features with the latest Firefox version.&lt;/p&gt;

&lt;h2 id=&quot;pagewaitfornetworkidleasync&quot;&gt;Page.WaitForNetworkIdleAsync&lt;/h2&gt;

&lt;p&gt;You now can wait for the network idle at any time using &lt;a href=&quot;https://www.puppeteersharp.com/api/PuppeteerSharp.Page.html#PuppeteerSharp_Page_WaitForNetworkIdleAsync_PuppeteerSharp_WaitForNetworkIdleOptions_&quot;&gt;Page.WaitForNetworkIdleAsync&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;pageemulateidlestateasync&quot;&gt;Page.EmulateIdleStateAsync&lt;/h2&gt;

&lt;p&gt;You can not only wait for network idle but emulate this state!
For instance:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;await page.EmulateIdleStateAsync(new EmulateIdleOverrides() {IsUserActive = true, IsScreenUnlocked = false});
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;drag-and-drop-support&quot;&gt;Drag and Drop support&lt;/h2&gt;

&lt;p&gt;Emulating drag and drop was quite hard to accomplish in the past. Now you have an API for that!&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;await Page.SetDragInterceptionAsync(true);
var draggable = await Page.QuerySelectorAsync(&quot;#drag&quot;);
var dropzone = await Page.QuerySelectorAsync(&quot;#drop&quot;);
await draggable.DragAndDropAsync(dropzone);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;pageemulatevisiondeficiencyasync&quot;&gt;Page.EmulateVisionDeficiencyAsync&lt;/h2&gt;

&lt;p&gt;Now you can emulate different vision deficiencies using &lt;a href=&quot;https://www.puppeteersharp.com/api/PuppeteerSharp.Page.html#PuppeteerSharp_Page_EmulateVisionDeficiencyAsync_PuppeteerSharp_VisionDeficiency_&quot;&gt;Page.EmulateVisionDeficiencyAsync&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;pageemulatecputhrottlingasync&quot;&gt;Page.EmulateCPUThrottlingAsync&lt;/h2&gt;

&lt;p&gt;You can also emulate CPU throttling with &lt;a href=&quot;https://www.puppeteersharp.com/api/PuppeteerSharp.Page.html#PuppeteerSharp_Page_EmulateCPUThrottlingAsync_System_Nullable_System_Decimal__&quot;&gt;Page.EmulateCPUThrottlingAsync&lt;/a&gt;!&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;await Page.EmulateCPUThrottlingAsync(100);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;its-our-puppeteer-sharp&quot;&gt;It’s our Puppeteer-Sharp!&lt;/h1&gt;

&lt;p&gt;Just a friendly reminder. Puppeteer-Sharp is an independent community project. It’s not being backed nor supported but any big (or small) corp. It depends on your support and contributions!&lt;br /&gt;
I want to thank &lt;a href=&quot;https://github.com/amaitland&quot;&gt;Alex Maitland&lt;/a&gt;, &lt;a href=&quot;https://github.com/jorbraken&quot;&gt;Fernando Bracamonte&lt;/a&gt;, &lt;a href=&quot;https://github.com/MantasVa&quot;&gt;MantasVa&lt;/a&gt;, &lt;a href=&quot;https://github.com/thomaschanneladvisor&quot;&gt;thomaschanneladvisor&lt;/a&gt;, &lt;a href=&quot;https://github.com/venky8951&quot;&gt;Venkatesh Prasad&lt;/a&gt;, &lt;a href=&quot;https://github.com/lofcz&quot;&gt;Matěj Štágl&lt;/a&gt;, &lt;a href=&quot;https://github.com/jpeirson&quot;&gt;jpeirson&lt;/a&gt;, &lt;a href=&quot;https://github.com/ericmutta&quot;&gt;Eric Mutta&lt;/a&gt;, &lt;a href=&quot;https://github.com/Chris-Dev-GH&quot;&gt;Chris-Dev-GH&lt;/a&gt;, &lt;a href=&quot;https://github.com/McKabue&quot;&gt;Kabue Charles&lt;/a&gt;, &lt;a href=&quot;https://github.com/kgar&quot;&gt;Kevin Lee Garner&lt;/a&gt;, &lt;a href=&quot;https://github.com/pirrmann&quot;&gt;Pierre Irrmann&lt;/a&gt;, &lt;a href=&quot;https://github.com/arnaudmaichac&quot;&gt;Arnaud Maichac&lt;/a&gt; and &lt;a href=&quot;https://github.com/brnbs&quot;&gt;brnbs&lt;/a&gt; for contributing to the project during this past year.&lt;/p&gt;

&lt;p&gt;I hope you can get more of my posts soon :)&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Puppeteer-Sharp 3 is here!</title>
    <link href="https://www.hardkoded.com/blog/puppeteer-sharp-3-is-here"/>
    <updated>2021-03-22T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/puppeteer-sharp-3-is-here</id>
    <content type="html">&lt;p&gt;I’m so excited to announce that, after eight months, Puppeteer-Sharp 3 is here!&lt;br /&gt;
&lt;img src=&quot;https://media.giphy.com/media/1yJEEsgy4q2bu/giphy.gif&quot; alt=&quot;party&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Getting Puppeteer-Sharp 3 out was quite a challenge. &lt;strong&gt;The lack of &lt;a href=&quot;https://github.com/sponsors/hardkoded&quot;&gt;sponsors&lt;/a&gt; on Puppeteer-Sharp&lt;/strong&gt; and the &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-joins-microsoft&quot;&gt;strong support of Microsoft on Playwright-Sharp&lt;/a&gt; made me put all my efforts on Playwright-Sharp in the past few months.&lt;/p&gt;

&lt;p&gt;But while a was writing the &lt;a href=&quot;https://www.uitestingwithpuppeteer.com/&quot;&gt;UI Testing with Puppeteer&lt;/a&gt; book, Puppeteer-Sharp got to 1 Million downloads (1 million downloads and only three sponsors), I said to myself: “Hey, let’s do something to celebrate!” So I decided to wrap up &lt;strong&gt;Experimental Firefox Support&lt;/strong&gt;. And that took us to v3!&lt;/p&gt;

&lt;h1 id=&quot;whats-new&quot;&gt;What’s new&lt;/h1&gt;

&lt;p&gt;PuppeteerSharp 3 is the first version that doesn’t fully map the same version (Puppeteer 3) upstream. It’s quite a Frankenstein, version-wise, but for good!&lt;br /&gt;
The first neat feature is that Puppeteer-Sharp is shipping Chromium 90.0.4403.0, which is the version that Puppeteer v8 is using.&lt;br /&gt;
The second new feature is &lt;a href=&quot;http://www.puppeteersharp.com/api/PuppeteerSharp.Page.html#PuppeteerSharp_Page_EmulateNetworkConditionsAsync_PuppeteerSharp_NetworkConditions_&quot;&gt;EmulateNetworkConditionsAsync&lt;/a&gt;, which was also a feature introduced in Puppeteer v8. This method allows the use to emulate networks such as 3G or 4G.&lt;br /&gt;
And last, but not least: Experimental Firefox support.&lt;br /&gt;
&lt;em&gt;You can find the complete changelog on the &lt;a href=&quot;https://github.com/hardkoded/puppeteer-sharp/releases/tag/v3.0.0&quot;&gt;release page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h1 id=&quot;the-state-of-the-fox&quot;&gt;The state of the Fox&lt;/h1&gt;

&lt;p&gt;Using PuppeteerSharp with Firefox is as easy as passing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product.Firefox&lt;/code&gt; to both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BrowserFetcher&lt;/code&gt; constructor, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LaunchAsync&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;BrowserFetcher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Firefox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;DownloadAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Puppeteer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;LaunchAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LaunchOptions&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;Headless&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Firefox&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;NewPageAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GoToAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://www.google.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ScreenshotAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;./google.png&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But remember, this is still experimental. Only 340 tests of the 681 are running on Firefox. It’s not a lot, but it’s something.&lt;br /&gt;
I also had to turn off the tests on Windows because of many zombie processes that remained open. I found this also happens on the node version.&lt;/p&gt;

&lt;p&gt;I want to thank &lt;a href=&quot;https://github.com/Androbin&quot;&gt;Robin Richtsfeld&lt;/a&gt; who implemented the emulate network conditions feature, among many other things, and &lt;a href=&quot;https://github.com/SimonCropp&quot;&gt;Simon Cropp&lt;/a&gt; who takes every project he touches to the next level.&lt;/p&gt;

&lt;h1 id=&quot;whats-next&quot;&gt;What’s next&lt;/h1&gt;

&lt;p&gt;I created the &lt;a href=&quot;https://github.com/hardkoded/puppeteer-sharp/projects/43&quot;&gt;V9 Project&lt;/a&gt;, so we can take PuppeteerSharp to what I guess will be the next Puppeteer version.&lt;/p&gt;

&lt;p&gt;But it will be hard to accomplish it without help. I hope I can get more support from contributors on this next project.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If you are reading this post and making money with Puppeteer-Sharp, &lt;strong&gt;I encourage you to &lt;a href=&quot;https://github.com/sponsors/hardkoded&quot;&gt;sponsor this project&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I hope we can catch Puppeteer soon and keep this project moving.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>UI testing with Puppeteer book officially released!</title>
    <link href="https://www.hardkoded.com/blog/ui-testing-with-puppeteer-released"/>
    <updated>2021-03-12T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/ui-testing-with-puppeteer-released</id>
    <content type="html">&lt;p&gt;I’m so thrilled to announce that my book &lt;a href=&quot;https://www.uitestingwithpuppeteer.com/&quot;&gt;UI testing with Puppeteer&lt;/a&gt;  has been published!&lt;br /&gt;
You can find it on &lt;a href=&quot;https://www.amazon.com/Testing-Puppeteer-end-end-automation/dp/180020678X/&quot;&gt;Amazon.com&lt;/a&gt; and 
&lt;a href=&quot;https://www.packtpub.com/product/ui-testing-with-puppeteer/9781800206786&quot;&gt;PacktPub.com&lt;/a&gt;. You will find all the details on &lt;a href=&quot;https://www.uitestingwithpuppeteer.com/&quot;&gt;the book website&lt;/a&gt;. Special Thanks to &lt;a href=&quot;https://fgiuliani.com/&quot;&gt;Facundo Giuliani&lt;/a&gt; for helping me set up a Jamstack website in a few hours.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://www.uitestingwithpuppeteer.com/images/B16113_Mockup%20cover.jpg&quot; alt=&quot;book&quot; width=&quot;300&quot; style=&quot;     margin-left: 25%; &quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;why-writing-a-book&quot;&gt;Why writing a book?&lt;/h1&gt;

&lt;p&gt;When &lt;a href=&quot;https://www.linkedin.com/in/kaustubh-manglurkar-871ba0167/&quot;&gt;Kaustubh Manglurkar&lt;/a&gt; from &lt;a href=&quot;https://www.packtpub.com/&quot;&gt;PacktPub&lt;/a&gt; reached me out asking me if I wanted to write a book, I didn’t know what to say. I wasn’t (and I don’t consider myself) a writer. But Kaustubh told me something that turned my doubts into an absolute yes. He told me that &lt;strong&gt;there was no book about Puppeteer in the market&lt;/strong&gt;.
That put extra pressure on the project. If I was going to write the first book about Puppeteer, &lt;strong&gt;it should cover most of the Puppeteer API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I can make a connection with TV shows like House or Grey’s Anatomy, where every chapter has its own plot, but there is an underlying story going on. The same happens with this book. Each chapter covers a topic, but the underlying story is the Puppeteer API.&lt;/p&gt;

&lt;h1 id=&quot;what-you-will-find-in-the-book&quot;&gt;What you will find in the book&lt;/h1&gt;

&lt;p&gt;This book will help you get started not only with Puppeteer but also with UI testing. Each chapter begins with the basic concepts before getting into the code. You will learn about &lt;strong&gt;automated testing&lt;/strong&gt; in general. You will learn what &lt;strong&gt;test runners&lt;/strong&gt; are and what you should expect from them.&lt;/p&gt;

&lt;p&gt;In Chapter 4, where you will learn to interact with the page, but you will also learn about &lt;strong&gt;CSS selectors and XPath&lt;/strong&gt;. You will even see a little bit about &lt;strong&gt;JavaScript closures&lt;/strong&gt; in Chapter 7. Chapter 8 is about &lt;strong&gt;environments emulation&lt;/strong&gt;, but I open the chapter talking about the internet ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This book is also for web developers!!&lt;/strong&gt;&lt;br /&gt;
In chapter 2, I talk about how web developers can use web automation as part of their workflow to test their UI components. In Chapter 7, We see how to &lt;strong&gt;generate content&lt;/strong&gt; with Puppeteer. Then in chapter 9, I talk about &lt;strong&gt;web scraping&lt;/strong&gt; and scraping ethics. The last chapter is about &lt;strong&gt;performance&lt;/strong&gt;, which is something we should all care about.&lt;/p&gt;

&lt;h1 id=&quot;filling-your-toolbox&quot;&gt;Filling your toolbox&lt;/h1&gt;

&lt;p&gt;This book will give you more than a complete picture of API and UI testing. Many chapters include a bonus track, where you will learn about a tool that will be helpful in your automation journey. 
These are the tools you will see in this book:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Github Actions.&lt;/li&gt;
  &lt;li&gt;VS Code debugging tools.&lt;/li&gt;
  &lt;li&gt;Checkly.&lt;/li&gt;
  &lt;li&gt;Checkly’s headless recorder.&lt;/li&gt;
  &lt;li&gt;Google Lighthouse.&lt;/li&gt;
  &lt;li&gt;Puppeteer-Cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;special-thanks&quot;&gt;Special thanks&lt;/h1&gt;

&lt;p&gt;It would be impossible to mention all the people involved in this project. I would miss many for sure. But I want to thank &lt;strong&gt;all the PacktPub crew&lt;/strong&gt;, in special &lt;a href=&quot;https://www.linkedin.com/in/kaustubh-manglurkar-871ba0167/&quot;&gt;Kaustubh Manglurkar&lt;/a&gt;, for making all this possible and my editor &lt;a href=&quot;https://www.linkedin.com/in/sofi-rogers-93b7177a/&quot;&gt;Sofi Rogers&lt;/a&gt; who basically taught me to write, not just a book, to write :). You are the best Sofi!&lt;br /&gt;
The job of the technical reviewers was impressive! &lt;a href=&quot;https://www.linkedin.com/in/davidrv87/&quot;&gt;David Rubio Vidal&lt;/a&gt; and &lt;a href=&quot;https://www.linkedin.com/in/jimfmunro/&quot;&gt;Jim Munro&lt;/a&gt; took the book to the next level.&lt;br /&gt;
Last but not least, thanks to my family and friends who supported me all these months. I love you all.&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;Maybe you are wondering, &lt;em&gt;“but Dario, how about &lt;strong&gt;.NET&lt;/strong&gt;? How about &lt;strong&gt;Playwright&lt;/strong&gt;? weren’t you working on Playwright?’”&lt;/em&gt; Well, if you are interested on seeing this book on .NET or  Playwright, don’t hesitate to send an email to PacktPub!&lt;/p&gt;

&lt;p&gt;One final and personal thought. One thing I learned in this process is that &lt;strong&gt;writing a book is hard!!&lt;/strong&gt;, really hard. It’s easy to be mean on social media for many of us when we don’t like something, and sometimes we forget to be thankful, even if we paid for the book we read. 
To all tech authors around: thank you for your hard work!&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Playwright Sharp Monthly Report - December 2020</title>
    <link href="https://www.hardkoded.com/blog/playwright-sharp-monthly-dec-2020"/>
    <updated>2020-12-04T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/playwright-sharp-monthly-dec-2020</id>
    <content type="html">&lt;p&gt;Wait, What happened with the November report? Well, well, November was a fun month. But here we are in the December report. Let’s get started!&lt;/p&gt;

&lt;h1 id=&quot;important-notice&quot;&gt;Important Notice.&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/Tdpbuz8KP0EpQfJR3T/giphy.gif?cid=ecf05e47pxmt5k97tbs7thqlrt0vus4sgbip66o3mj4u2fe1&amp;amp;rid=giphy.gif&quot; alt=&quot;alert&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As you might know, &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-joins-microsoft&quot;&gt;Playwright sharp is now part of the Microsoft family&lt;/a&gt;. So you should read these reports as unofficial reports. I will share here, what I was working on, what cool stuff I learned, and how I’m going to contribute to the project in the next month. I will use that line that we see quite often in Twitter bios: Opinions are my own.&lt;/p&gt;

&lt;p&gt;Too much talk. Let’s got to the fun part.&lt;/p&gt;

&lt;h1 id=&quot;whats-new&quot;&gt;What’s new&lt;/h1&gt;

&lt;p&gt;Since the last report, we got three new versions: &lt;a href=&quot;https://github.com/microsoft/playwright-sharp/releases/tag/v0.151.0&quot;&gt;v0.151.0&lt;/a&gt;, &lt;a href=&quot;https://github.com/microsoft/playwright-sharp/releases/tag/v0.152.0&quot;&gt;v0.152.0&lt;/a&gt;, and &lt;a href=&quot;https://github.com/microsoft/playwright-sharp/releases/tag/v0.162.0&quot;&gt;v0.162.0&lt;/a&gt;.
If you read &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-oct-2020&quot;&gt;my previous report&lt;/a&gt;, you will know that those versions map Playwright’s versions v1.5.1, v1.5.2, and 1.6.2, respectively.&lt;/p&gt;

&lt;p&gt;PlaywrightSharp now offers some pretty cool new features, such as &lt;strong&gt;Video Screencasts&lt;/strong&gt;, &lt;strong&gt;HAR recording&lt;/strong&gt;,  &lt;strong&gt;WebSockets inspection&lt;/strong&gt; and &lt;strong&gt;Tap support&lt;/strong&gt; on mobile emulation.&lt;br /&gt;
Do you want to take a look at these new features? Comment this post with the feature you’d like to see, and I’ll create a post just for you, well, for everybody.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/MXQmDs1c8oAOVkIYHz/giphy.gif&quot; alt=&quot;comment&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;driver-and-browser-install-refactor&quot;&gt;Driver and browser install refactor&lt;/h1&gt;

&lt;p&gt;PlaywrightSharp has two big dependencies: The playwright driver, a bundled Node.JS app with all the playwright logic, and the browsers used to perform the automation.&lt;br /&gt;
We have one driver for each platform: OSX, Linux, Win64, and Win32. And we have three browsers: Chromium, Firefox, and WebKit, which are also platform-specific.&lt;/p&gt;

&lt;h2 id=&quot;the-first-approach&quot;&gt;The first approach&lt;/h2&gt;

&lt;p&gt;The first approach was to embed the drivers inside the PlaywrightSharp.dll. When you call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Playwright.CreateAsync&lt;/code&gt;. We would extract the driver for your platform and copy it to your working directory.
Then you would need to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Playwright.InstallAsync&lt;/code&gt; to install the browsers if required.&lt;/p&gt;

&lt;p&gt;This approach had a few downsides.&lt;br /&gt;
First, We were shipping a 60mb DLL file. We could say: “ok, but we need those drivers”. But you needed only the driver for your platform, not all the drivers. This approach also prevented us from shipping platform-specific libraries, e.g., for .NET 5.&lt;br /&gt;
Second, we were writing files in runtime. That’s not ideal. We have the risk of not being able to write to disk in some scenarios.&lt;br /&gt;
Last, we were doing install tasks in runtime. If you were using it in production, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Playwright.InstallAsync&lt;/code&gt; would cause the first call to your app to take a few minutes to download all the required browsers. Not ideal at all.&lt;/p&gt;

&lt;p&gt;We tried to remove the need for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InstallAsync&lt;/code&gt; task with a dotnet tool. It did the work, but the potential problem there was that the dotnet tool was downloading the browsers for the version referenced in the tool, which might not be the same as the version your app needed.&lt;/p&gt;

&lt;p&gt;There should be a better way.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media1.giphy.com/media/Ns1zcDF7zA4co/giphy.gif?cid=ecf05e47qt5hckevtj1lf40bbqt81ha7xir8pvaj0aj6l11h&amp;amp;rid=giphy.gif&quot; alt=&quot;better way&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;a-new-approach&quot;&gt;A new approach&lt;/h2&gt;

&lt;p&gt;v0.162.0 now relies on two features:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Nuget runtimes folder.&lt;/li&gt;
  &lt;li&gt;Custom Build targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nuget has something called &lt;a href=&quot;https://docs.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks?WT.mc_id=DT-MVP-5003814#architecture-specific-folders&quot;&gt;architecture-specific folders&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If you have architecture-specific assemblies, that is, separate assemblies that target ARM, x86, and x64, you must place them in a folder named runtimes within sub-folders named {platform}-{architecture}\lib{framework} or {platform}-{architecture}\native.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So now, instead of embedding the drivers into the DLL, we are shipping the drivers inside the runtimes folder:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;\runtimes 
    \osx
        \native 
    \unix
        \native 
    \win-x64
        \native 
    \win-x86
        \native 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Nuget will add a build task to copy those files depending on the &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props?WT.mc_id=DT-MVP-5003814#runtimeidentifier&quot;&gt;RuntimeIdentifier&lt;/a&gt; or on the &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish?WT.mc_id=DT-MVP-5003814&quot;&gt;runtime used on dotnet publish&lt;/a&gt;.&lt;br /&gt;
If you don’t have a RuntimeIdentifer and don’t use the publish command (this would be a typical unit test scenario), all the drivers will be copied under a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimes&lt;/code&gt; folder. That’s why if we find the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;playwright-cli&lt;/code&gt; in the current directory, we use it; if not, we try to go to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimes&lt;/code&gt; folder, looking for a platform-specific driver.&lt;/p&gt;

&lt;p&gt;That gives us a PlaywrightSharp.dll file of only 500kb, which would allow us to ship multi-target binaries, e.g., for .NET 5. And we also removed the need for writing files at runtime.&lt;/p&gt;

&lt;p&gt;I also learn that you can &lt;a href=&quot;https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package?WT.mc_id=DT-MVP-5003814#include-msbuild-props-and-targets-in-a-package&quot;&gt;add targets inside Nuget packages&lt;/a&gt;. That means that you can inject your own tasks into the build process.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media2.giphy.com/media/YQitE4YNQNahy/giphy.gif?cid=ecf05e47je5sy622q1yar960f2r1cv0lbilasndalwin4w9i&amp;amp;rid=giphy.gif&quot; alt=&quot;hacker&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I created two targets that will run as part of your build process. The first task will run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;playwright-cli install&lt;/code&gt; command, installing the required browsers. At build time 😯.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &amp;lt;Target Name=&quot;InstallBrowsers&quot; AfterTargets=&quot;CopyFilesToOutputDirectory&quot;&amp;gt;
     &amp;lt;Message Text=&quot;Playwright is checking browser dependencies&quot;&amp;gt;
     &amp;lt;/Message&amp;gt;
     &amp;lt;Exec Condition=&quot;$([MSBuild]::IsOSPlatform(&apos;Linux&apos;))&quot; WorkingDirectory=&quot;$(MSBuildThisFileDirectory)..\..\runtimes\unix$(NativeFolder)&quot; Command=&quot;./playwright-cli install&quot; /&amp;gt;
     &amp;lt;Exec Condition=&quot;$([MSBuild]::IsOSPlatform(&apos;OSX&apos;))&quot; WorkingDirectory=&quot;$(MSBuildThisFileDirectory)..\..\runtimes\osx$(NativeFolder)&quot; Command=&quot;./playwright-cli install&quot; /&amp;gt;
     &amp;lt;Exec Condition=&quot;&apos;$(PlatformTarget)&apos; != &apos;x86&apos; AND $([MSBuild]::IsOSPlatform(&apos;Windows&apos;))&quot; WorkingDirectory=&quot;$(MSBuildThisFileDirectory)\..\..\runtimes\win-x64$(NativeFolder)&quot; Command=&quot;playwright-cli.exe install&quot; /&amp;gt;
     &amp;lt;Exec Condition=&quot;&apos;$(PlatformTarget)&apos; == &apos;x86&apos; AND $([MSBuild]::IsOSPlatform(&apos;Windows&apos;))&quot; WorkingDirectory=&quot;$(MSBuildThisFileDirectory)\..\..\runtimes\win-x89$(NativeFolder)&quot; Command=&quot;playwright-cli.exe install&quot; /&amp;gt;
 &amp;lt;/Target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The second target is relatively new. It will copy the drivers if you try to use Playwright inside a Xamarin.Mac application.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &amp;lt;Target Name=&quot;CopyPlaywrightDriversToMonoBundle&quot; AfterTargets=&quot;CopyFilesToOutputDirectory&quot; Condition=&quot;$(TargetFrameworkIdentifier) == &apos;Xamarin.Mac&apos;&quot;&amp;gt;
     &amp;lt;Message Text=&quot;Copy drivers to MonoBundle&quot; /&amp;gt;
     &amp;lt;CreateItem Include=&quot;$(MSBuildThisFileDirectory)\..\..\runtimes\osx\native\*.*&quot;&amp;gt;
         &amp;lt;Output TaskParameter=&quot;Include&quot; ItemName=&quot;PlaywrightDriverFiles&quot; /&amp;gt;
     &amp;lt;/CreateItem&amp;gt;
     &amp;lt;Message Text=&quot;$(AppBundleDir)&quot; /&amp;gt;
     &amp;lt;Message Text=&quot;@(PlaywrightDriverFiles)&quot; /&amp;gt;
     &amp;lt;Copy SourceFiles=&quot;@(PlaywrightDriverFiles)&quot; DestinationFolder=&quot;$(AppBundleDir)/Contents/MonoBundle/&quot; /&amp;gt;
 &amp;lt;/Target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;shipping-process&quot;&gt;Shipping process&lt;/h2&gt;

&lt;p&gt;With these changes, we wouldn’t need to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InstallAsync&lt;/code&gt; anymore.&lt;br /&gt;
If you are using PlaywrightSharp for testing, you are getting the browsers at build time.&lt;br /&gt;
If you are using PlaywrightSharp in Docker and you use any &lt;a href=&quot;https://hub.docker.com/_/microsoft-playwright&quot;&gt;official Docker image&lt;/a&gt;, you will get the browser for free .&lt;br /&gt;
If you are shipping it to a custom server, you will get the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;playwright-cli&lt;/code&gt; binary in your bin folder. You just need to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.\bin\playwright-cli.exe install&lt;/code&gt; in Windows or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./bin/playwright-cli install&lt;/code&gt; in OSX/Linux as part of your deploy script.&lt;/p&gt;

&lt;p&gt;I hope all these changes remove some barriers and deploy issues that some users were having.&lt;/p&gt;

&lt;h1 id=&quot;bonus-track&quot;&gt;Bonus track&lt;/h1&gt;

&lt;p&gt;As we now have the driver in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; folder, we can use the C# code generator that comes with the CLI.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/lsU7mOh76j4QM/giphy.gif?cid=ecf05e47zomi5klflcf6e732w0l4ecbihlip83gppnsmr2gt&amp;amp;rid=giphy.gif&quot; alt=&quot;what&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If you go to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin&lt;/code&gt; folder, you can run this command:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./playwright-cli codegen --target=csharp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And voilà!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kblok/kblok.github.io/master/img/report-dec-2020/codegen.gif&quot; alt=&quot;code gen&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I hope you enjoy this new tool. It’s still quite raw. But we’ll get better.&lt;/p&gt;

&lt;h1 id=&quot;activity&quot;&gt;Activity&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;Repository Stars: 250 (prev 165) +51%&lt;/li&gt;
  &lt;li&gt;Repository Forks: 26 (prev 14) +85%&lt;/li&gt;
  &lt;li&gt;Nuget downloads: 2878&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;I’m looking forward to getting more feedback. The issues tab in GitHub is open for all of you to share your ideas and thoughts.&lt;br /&gt;
Don’t forget to follow me on Twitter &lt;a href=&quot;https://twitter.com/hardkoded&quot;&gt;@hardkoded&lt;/a&gt; to get more updates.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;

&lt;h4 id=&quot;previous-reports&quot;&gt;Previous Reports&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-oct-2020&quot;&gt;Oct 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-sep-2020&quot;&gt;Sep 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jul-2020&quot;&gt;Jul 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jun-2020&quot;&gt;Jun 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-may-2020&quot;&gt;May 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-apr-2020&quot;&gt;April 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-march-2020&quot;&gt;March 2020&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  
  <entry>
    <title>Playwright Sharp Monthly Report - October 2020</title>
    <link href="https://www.hardkoded.com/blog/playwright-sharp-monthly-oct-2020"/>
    <updated>2020-10-06T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/playwright-sharp-monthly-oct-2020</id>
    <content type="html">&lt;p&gt;We accomplish two big milestones in September. &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-joins-microsoft&quot;&gt;Microsoft adopted Playwright Sharp&lt;/a&gt; and now is part of the Playwright family.&lt;br /&gt;
We shipped v0.142.0,  which is the first version having full feature match with Playwright!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media1.giphy.com/media/PSyqVLhwDvjXO/giphy.gif?cid=ecf05e47ehdpfrsgwhc0yi5v6wzm6o4e543tqj0rbe30ya7v&amp;amp;rid=giphy.gif&quot; alt=&quot;boom&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;no-v1-yet&quot;&gt;No v1 yet?&lt;/h1&gt;

&lt;p&gt;We decided to leave PlaywrightSharp in v0, so we can give &lt;strong&gt;you&lt;/strong&gt; the chance to send us feedback and help us shape the API.&lt;br /&gt;
Until we decide to make it v1, the minor version will map to the Playwright version. In this case, v0.142.0 maps to v1.4.2 in Playwright.&lt;/p&gt;

&lt;h1 id=&quot;breaking-changes&quot;&gt;Breaking Changes&lt;/h1&gt;

&lt;p&gt;Now that Playwright Sharp is part of the Playwright family, there must be some consistency between Playwright bindings. But, you shouldn’t feel as if Playwright Sharp was generated by some code conversion tool. Playwright Sharp needs to be a first-class C# library.&lt;/p&gt;

&lt;p&gt;During this evaluation, &lt;strong&gt;we decided to drop most option classes&lt;/strong&gt;, like ClickOptions, PdfOptions, etc. We left a few classes related to launch settings, like LaunchOptions. We considered you might want to load those options from a settings file.&lt;/p&gt;

&lt;h2 id=&quot;our-tools-shapes-how-we-code&quot;&gt;Our tools shapes how we code&lt;/h2&gt;

&lt;p&gt;The tools we use shape how we code. If you use Entity Framework, you will use Linq a lot. If you use Playwright Sharp, we will want you to take advantage of &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments?WT.mc_id=DT-MVP-5003814&quot;&gt;named arguments&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, if we drop &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PdfOptions&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Page.GetPdfAsync&lt;/code&gt;, we woulnd’t like you to do this:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetPdfAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;div&amp;gt;Test footer template&amp;lt;/div&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Margin&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Bottom&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;1in&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Top&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;2in&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But this&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetPdfAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;displayHeaderFooter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;headerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;footerTemplate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;div&amp;gt;Test footer template&amp;lt;/div&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Margin&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Bottom&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;1in&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Top&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;2in&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;printBackground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;playwright-sharp-dotnet-tool&quot;&gt;playwright-sharp dotnet tool&lt;/h1&gt;

&lt;p&gt;One of the weak spots of Playwright Sharp was the need for a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;await Playwright.InstallAsync();&lt;/code&gt; in runtime. That wasn’t ideal.&lt;br /&gt;
Now we can get rid of that line using the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;playwright-sharp&lt;/code&gt; dotnet tool. It’s as easy as installing the tool and running the install command:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;dotnet&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sharp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tool&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sharp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;browsers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This dotnet tool opens the door to many other CLI features we can implement in the future.&lt;/p&gt;

&lt;h1 id=&quot;playwrightsharpdev&quot;&gt;playwrightsharp.dev&lt;/h1&gt;

&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/MeirBlachman&quot;&gt;Meir Blachman&lt;/a&gt; we now have a site!&lt;br /&gt;
This is still a work in progress, but we started to build the website. You can take a look at the &lt;a href=&quot;https://playwrightsharp.dev/api/index.html&quot;&gt;API&lt;/a&gt; and a few &lt;a href=&quot;https://playwrightsharp.dev/examples/index.html&quot;&gt;Examples&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;activity&quot;&gt;Activity&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;Repository Stars: 165 (prev 140) +17%&lt;/li&gt;
  &lt;li&gt;Repository Forks: 14 (prev 10) +40%&lt;/li&gt;
  &lt;li&gt;Nuget downloads: 550&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;Getting close to our first v1 version, I’m looking forward to getting more feedback. The issues tab in GitHub is open for all of you to share your ideas and thoughts.&lt;br /&gt;
Don’t forget to follow me on Twitter &lt;a href=&quot;https://twitter.com/hardkoded&quot;&gt;@hardkoded&lt;/a&gt; to get more updates.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;

&lt;h4 id=&quot;previous-reports&quot;&gt;Previous Reports&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-sep-2020&quot;&gt;Sep 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jul-2020&quot;&gt;Jul 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jun-2020&quot;&gt;Jun 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-may-2020&quot;&gt;May 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-apr-2020&quot;&gt;April 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-march-2020&quot;&gt;March 2020&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  
  <entry>
    <title>Playwright Sharp joins the Microsoft family</title>
    <link href="https://www.hardkoded.com/blog/playwright-sharp-joins-microsoft"/>
    <updated>2020-09-24T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/playwright-sharp-joins-microsoft</id>
    <content type="html">&lt;p&gt;It is my pleasure to announce that today Microsoft forked Playwright Sharp!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media2.giphy.com/media/KYElw07kzDspaBOwf9/giphy.gif?cid=ecf05e47jd4b32h2i1qpa0cjydd4upcfa9a1ndq84mob8b0h&amp;amp;rid=giphy.gif&quot; alt=&quot;Party&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;wait-did-you-give-the-project-away&quot;&gt;Wait, did you give the project away?&lt;/h1&gt;

&lt;p&gt;No, I didn’t. Because it was never mine.&lt;/p&gt;

&lt;h2 id=&quot;its-our-project&quot;&gt;It’s our project&lt;/h2&gt;

&lt;p&gt;Playwright Sharp uses the MIT License. That means that Playwright Sharp is mine, and it’s also yours, and it’s also Microsoft’s. It’s ours.&lt;/p&gt;

&lt;p&gt;You can do the same! You can fork the project and make it better.&lt;/p&gt;

&lt;h2 id=&quot;playwright-team-rocks&quot;&gt;Playwright team rocks!&lt;/h2&gt;

&lt;p&gt;They could just copy my code and ship it with a different name. But instead, they opened the doors of their team and embraced the  Playwright Sharp project.&lt;br /&gt;
I think Playwright Sharp fits perfectly in the vision the team has for the product.&lt;/p&gt;

&lt;h1 id=&quot;but-why&quot;&gt;But, why?&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;https://media2.giphy.com/media/eauCbbW6MvqKI/giphy.gif?cid=ecf05e47364le8v0im7wa8ynnxh3r03l47k26mnkc8kylz1o&amp;amp;rid=giphy.gif&quot; alt=&quot;Why&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I think it’s what needs to be done to take this project to the next level. 
The .NET community needs to grown in their relationship with the open-source. &lt;a href=&quot;https://twitter.com/Aaronontheweb&quot;&gt;Aaron Stannard&lt;/a&gt; explained very well on his post &lt;a href=&quot;https://aaronstannard.com/next-decade-dotnet/&quot;&gt;The Next Decade of .NET Open Source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When a .NET team needs to add a third-party tool to their toolbox, the management evaluates the long term support and the final decision tends to be: “I’d better use a tool from Microsoft than from a random Argentinian guy”.&lt;/p&gt;

&lt;p&gt;We developers also have some responsibility. The biggest risk for a .NET developer involved in open-source is Microsoft creating a similar project. And it’s not Microsoft fault at all. But developers would pick Entity Framework over Nhibernate, the default ASP.NET DI over Windsor Castle, etc.&lt;br /&gt;
I love how Microsoft embraced open-source and became a big player in the open-source community. But I think the .NET community has a lot to learn yet.&lt;/p&gt;

&lt;p&gt;Another reason to move the project is that, despite the relative popularity of Puppeteer-Sharp, I wasn’t able to build a stable team.
My hope with this move is to serve way more developers compared to Puppeteer-Sharp and also get more people involved in the project.&lt;/p&gt;

&lt;p&gt;Remember, it’s not my project. It’s OURS.&lt;/p&gt;

&lt;h1 id=&quot;are-you-leaving-the-project&quot;&gt;Are you leaving the project?&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/Ga6P43loQ0kE/giphy.gif?cid=ecf05e47rio3nr1ut1dvgwfxfu80cwdgv0a1i0iky724e5o1&amp;amp;rid=giphy.gif&quot; alt=&quot;Hell no&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Hell no! The roadmap will now be aligned with Playwright, which will be great for Playwright Sharp. &lt;a href=&quot;https://twitter.com/arjunattam&quot;&gt;Arjun Attam&lt;/a&gt; is an awesome PM, and I hope he can help me take Playwright-Sharp to the next level.&lt;/p&gt;

&lt;h1 id=&quot;the-future&quot;&gt;The future&lt;/h1&gt;

&lt;p&gt;My dream is to make Playwright-Sharp a first-class automation tool for the .NET community, and there is no better partner than Microsoft to accomplish that.&lt;/p&gt;

&lt;h1 id=&quot;give-some-love&quot;&gt;Give some love!&lt;/h1&gt;

&lt;p&gt;So, Playwright Sharp has a new home: &lt;a href=&quot;https://github.com/microsoft/playwright-sharp&quot;&gt;https://github.com/microsoft/playwright-sharp&lt;/a&gt;. Let’s go there and give it some love ⭐️.&lt;/p&gt;

&lt;p&gt;Happy automation!&lt;br /&gt;
Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>My programming language is the best!</title>
    <link href="https://www.hardkoded.com/blog/my-programming-language-is-the-best"/>
    <updated>2020-09-16T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/my-programming-language-is-the-best</id>
    <content type="html">&lt;p&gt;I’ve been porting &lt;a href=&quot;https://github.com/puppeteer/puppeteer&quot;&gt;Puppeteer&lt;/a&gt; code (to &lt;a href=&quot;https://github.com/hardkoded/puppeteer-sharp&quot;&gt;puppeteer-sharp&lt;/a&gt;) from javascript to C# since late 2017 and &lt;a href=&quot;https://github.com/microsoft/playwright&quot;&gt;Playwright&lt;/a&gt; code (to &lt;a href=&quot;https://github.com/hardkoded/playwright-sharp&quot;&gt;playwright-sharp&lt;/a&gt;) since early 2020.&lt;/p&gt;

&lt;p&gt;One of the things I learned is that programming languages are not about computer instructions, but human interaction.&lt;/p&gt;

&lt;p&gt;Migrating code from Javascript/Typescript to C# is not about translating statements. It’s about figuring out how C# developers express what a Javascript developer tried to communicate.&lt;/p&gt;

&lt;p&gt;Each programming language has a community, a background, influencers, and frameworks that shape how programs are written.&lt;br /&gt;
We can’t deny that javascript was born to solve simple HTML interactions. And we can’t deny that C# was born as a new language to the Visual Basic community.&lt;/p&gt;

&lt;p&gt;In the same way that spoken languages follow a community, their habits, and their way of living. Programming languages follow a programming community.&lt;/p&gt;

&lt;p&gt;What makes a language good or bad is also biased. We consider French romantic because of French music. We might also consider other languages aggressive because that’s the language of bad guys in Hollywood movies.&lt;/p&gt;

&lt;p&gt;In the same way, we might consider some languages cooler than others because of influencers and Twitter hype.&lt;/p&gt;

&lt;p&gt;My programming language is the best because it represents my community and how we communicate with each other.&lt;/p&gt;

&lt;p&gt;The next time you get in touch with a language you are not familiar with, instead of thinking if it’s bad or good, try to understand from that community and embrace this diversity we have in the industry.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Playwright Sharp Monthly Report - September 2020</title>
    <link href="https://www.hardkoded.com/blog/playwright-sharp-monthly-sep-2020"/>
    <updated>2020-09-08T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/playwright-sharp-monthly-sep-2020</id>
    <content type="html">&lt;p&gt;Two months passed since our &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jul-2020&quot;&gt;last monthly report back in July&lt;/a&gt;. I’m so sorry about that. But, in the past two months, we were able to merge 161 pull requests!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://64.media.tumblr.com/tumblr_ma0do9CrEh1qi2sux.gif&quot; alt=&quot;wow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Those 161 PRs took Playwright-Sharp to &lt;strong&gt;v0.111.2&lt;/strong&gt;! You can download this version from Nugget.&lt;/p&gt;

&lt;h2 id=&quot;a-massive-change-under-the-hood&quot;&gt;A massive change under the hood&lt;/h2&gt;

&lt;p&gt;Playwright-Sharp now uses a playwright driver to perform most of its operations. That will help Playwright-Sharp to keep Playwright pace, and it will also give Playwright users consistency across different bindings.
BTW, Do you need to run Playwright in Python? We now have &lt;a href=&quot;https://github.com/microsoft/playwright-python&quot;&gt;playwright-python&lt;/a&gt; :)&lt;/p&gt;

&lt;h3 id=&quot;would-i-need-to-install-nodejs-to-use-the-new-version-of-puppeteer-sharp&quot;&gt;Would I need to install Node.JS to use the new version of Puppeteer-Sharp?&lt;/h3&gt;

&lt;p&gt;No, the driver will have node.js embedded.&lt;/p&gt;

&lt;h2 id=&quot;about-the-version-number&quot;&gt;About the version number&lt;/h2&gt;

&lt;p&gt;The version number might sound quite odd. Let’s unwrap it.&lt;br /&gt;
Although this version matches Playwright 1.1.1, I want to leave some room for this API to change. That’s why it’s not v1 yet.&lt;/p&gt;

&lt;p&gt;Its minor version is 111 because it matches v1.1.1, and tts revision is 2 because… you know… things can fail.&lt;/p&gt;

&lt;p&gt;I believe that the next version will be v1.4 with a stable API.&lt;/p&gt;

&lt;h2 id=&quot;breaking-changes&quot;&gt;Breaking changes&lt;/h2&gt;

&lt;p&gt;The first change you will notice is that we merged all the previous four packages we had (PlaywrightSharp.Abstractions, PlaywrightSharp.Chromium, PlaywrightSharp.Firefox, and PlaywrightSharp.Webkit) into one single PlaywrightSharp package.&lt;/p&gt;

&lt;p&gt;There are not many breaking changes in the code. But the way we now install and launch a browser has changed.&lt;/p&gt;

&lt;p&gt;Let’s look at this example&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Installing playwright&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;InstallAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;playwright&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CreateAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Chromium&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;LaunchAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headless&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;NewPageAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Navigating google&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GoToAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://www.google.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Generating PDF&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetPdfAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Combine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Directory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetCurrentDirectory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;google.pdf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Export completed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Playwright.InstallAsync&lt;/code&gt; will install all the required browsers. It replaces the old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BrowserFetcher&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;InstallAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As I mentioned before, PlaywrightSharp now relies on the Playwright driver. That’s why we now need to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Playwright.CreateAsync&lt;/code&gt;. This method will create a new driver process that Playwright-Sharp will use to interact with the browser.&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;playwright&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CreateAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That playwright instance will give us tools to launch different browsers:&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;playwright&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Chromium&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;LaunchAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headless&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;From there, everything would look like the code you had before.&lt;/p&gt;

&lt;h1 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h1&gt;

&lt;p&gt;I hope we can ship v1.4 during September. That would be our first v1 version. Wish me luck.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/3oeSAz6FqXCKuNFX6o/giphy.gif&quot; alt=&quot;Good luck&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;activity&quot;&gt;Activity&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;Repository Stars: 140 (prev 117) +19%&lt;/li&gt;
  &lt;li&gt;Repository Forks: 10 (prev 8) +25%&lt;/li&gt;
  &lt;li&gt;Nuget downloads: 662&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;Getting close to our first v1 version, I’m looking forward to getting more feedback. The issues tab in GitHub is open for all of you to share your ideas and thoughts.&lt;br /&gt;
Don’t forget to follow me on Twitter &lt;a href=&quot;https://twitter.com/hardkoded&quot;&gt;@hardkoded&lt;/a&gt; to get more updates.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;

&lt;h4 id=&quot;previous-reports&quot;&gt;Previous Reports&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jul-2020&quot;&gt;Jul 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jun-2020&quot;&gt;Jun 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-may-2020&quot;&gt;May 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-apr-2020&quot;&gt;April 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-march-2020&quot;&gt;March 2020&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  <entry>
    <title>Playwright Sharp Monthly Report - July 2020</title>
    <link href="https://www.hardkoded.com/blog/playwright-sharp-monthly-jul-2020"/>
    <updated>2020-07-06T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/playwright-sharp-monthly-jul-2020</id>
    <content type="html">&lt;p&gt;As promised last month, We shipped PlaywrightSharp.Firefox v0.10! You can read more about this version on &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-firefox-010&quot;&gt;my previous post&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;playwright-sharp-as-a-playwright-client&quot;&gt;Playwright-Sharp as a Playwright client?&lt;/h2&gt;

&lt;p&gt;We are working with the Playwright team on a Playwright RPC Server.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media3.giphy.com/media/tu54GM19sqJOw/giphy.gif?cid=ecf05e474bb123a4008217cce4e23050f901bff9caa27a8e&amp;amp;rid=giphy.gif&quot; alt=&quot;what?&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This Playwright server would package a (Node.JS) playwright instance, exposing its functionality through pipes. &lt;a href=&quot;https://www.hardkoded.com/blog/interprocess-communication&quot;&gt;Remember I talked about pipes?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we land with this idea (this is still a proof of concept), we will be able to speed up  Playwright-Sharp development, allowing us to work on &lt;a href=&quot;https://github.com/hardkoded/playwright-sharp/projects/4&quot;&gt;many other things I have dreamed for this library&lt;/a&gt;. I want Playwright-Sharp to be not only a cool library, but the best automation suite for .NET developers.&lt;/p&gt;

&lt;p&gt;I’m sharing with you this internals because that’s the purpose of these reports. But Playwright-Sharp wouldn’t feel like just a client. It will be a first-class citizen, and you will be able to use it in the same way you’d use playwright.&lt;/p&gt;

&lt;p&gt;So maybe, and this a huge maybe, we would be able to ship a V1 in the next 40 days.&lt;/p&gt;

&lt;h1 id=&quot;activity&quot;&gt;Activity&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;Repository Stars: 117 (prev 94) +24%&lt;/li&gt;
  &lt;li&gt;Repository Forks: 8 (prev 7) +14%&lt;/li&gt;
  &lt;li&gt;Nuget downloads: 274&lt;/li&gt;
  &lt;li&gt;Contributors: 1&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;contributors&quot;&gt;Contributors&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://twitter.com/MeirBlachman&quot;&gt;Meir Blachman&lt;/a&gt; is doing most of the job on the Firefox project, so if you get to use Playwright-Sharp with Firefox, thanks Meir!&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;Now that we shipped our first versions, I’m looking forward to getting more feedback. The issues tab in GitHub is open for all of you to share your ideas and thoughts.&lt;br /&gt;
Don’t forget to follow me on Twitter &lt;a href=&quot;https://twitter.com/hardkoded&quot;&gt;@hardkoded&lt;/a&gt; to get more updates.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;

&lt;h4 id=&quot;previous-reports&quot;&gt;Previous Reports&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jun-2020&quot;&gt;Jun 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-may-2020&quot;&gt;May 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-apr-2020&quot;&gt;April 2020&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-march-2020&quot;&gt;March 2020&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  <entry>
    <title>PlaywrightSharp v0.10 for Firefox is here!</title>
    <link href="https://www.hardkoded.com/blog/playwright-sharp-firefox-010"/>
    <updated>2020-06-19T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/playwright-sharp-firefox-010</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://media.giphy.com/media/xsE65jaPsUKUo/giphy.gif&quot; alt=&quot;firefox&quot; /&gt;&lt;br /&gt;
Firefox joined to the PlaywrightSharp family!&lt;/p&gt;

&lt;h2 id=&quot;what-can-i-do-with-playwrightsharpfirefox&quot;&gt;What can I do with PlaywrightSharp.Firefox?&lt;/h2&gt;

&lt;p&gt;You can do lots of things.&lt;br /&gt;
&lt;img src=&quot;https://media.giphy.com/media/qkdTy6tTmF7Xy/giphy.gif&quot; alt=&quot;a lot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Seriously!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://media.giphy.com/media/Ful8UzCFYAjlu/giphy.gif&quot; alt=&quot;seriously&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You should be able to do almost everything you would do with his older brother PuppeteerSharp or his younger brother PlaywrightSharp.Chromium.&lt;br /&gt;
This is how you can take a screenshot in PlaywrightSharp &lt;strong&gt;NOW&lt;/strong&gt;!&lt;/p&gt;

&lt;div class=&quot;language-cs highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;options&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LaunchOptions&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Headless&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Downloading Firefox&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firefox&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;FirefoxBrowserType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firefox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;CreateBrowserFetcher&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;DownloadAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Navigating google&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firefox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;LaunchAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DefaultContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;NewPageAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GoToAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://www.microsoft.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Taking Screenshot&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteAllBytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;Combine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Directory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GetCurrentDirectory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;microsoft.png&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ScreenshotAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;Console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Export completed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;is-there-anything-i-cant-do&quot;&gt;Is there anything I can’t do?&lt;/h2&gt;

&lt;p&gt;There are only two features not implemented on PlaywrightSharp.Firefox: PDF generator and Tracing.&lt;/p&gt;

&lt;h2 id=&quot;can-i-use-this-package-in-production&quot;&gt;Can I use this package in Production?&lt;/h2&gt;

&lt;p&gt;As &lt;a href=&quot;https://www.hardkoded.com/blog/playwright-sharp-monthly-jun-2020&quot;&gt;I mentioned when we launched Playwrightsharp.Chromium&lt;/a&gt;, If you start using PlaywrightSharp now, the API is going to change, guaranteed, but you will jump into a project that will have way more support and new features.&lt;/p&gt;

&lt;p&gt;PuppeteerSharp doesn’t support Firefox yet. So I believe PlaywrightSharp.Firefox, even in its 0.10 version, is the best library to automate Firefox in .NET.&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final words&lt;/h1&gt;

&lt;p&gt;Now that we shipped our first version, I’m looking forward to getting more feedback. The issues tab in GitHub is open for all of you to share your ideas and thoughts.&lt;br /&gt;
Don’t forget to follow me on Twitter &lt;a href=&quot;https://twitter.com/hardkoded&quot;&gt;@hardkoded&lt;/a&gt; to get more updates.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Implementing interprocess communications between .NET and Node.JS</title>
    <link href="https://www.hardkoded.com/blog/interprocess-communication"/>
    <updated>2020-06-08T00:00:00+00:00</updated>
    <id>https://www.hardkoded.com/blog/interprocess-communication</id>
    <content type="html">&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;/h1&gt;

&lt;p&gt;If you want to implement interprocess communication, you will end up reading about &lt;a href=&quot;https://www.tutorialspoint.com/inter_process_communication/inter_process_communication_pipes.htm&quot;&gt;pipes&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/File_descriptor&quot;&gt;file descriptors&lt;/a&gt;. The problem I found is that .NET does a great job hiding all these concepts from us. I don’t know about you, but I didn’t even know much about these concepts until I needed to implement some interprocess communication.&lt;/p&gt;

&lt;h1 id=&quot;motivation&quot;&gt;Motivation&lt;/h1&gt;

&lt;p&gt;Most headless browsers, e.g. Chromium, have the ability to communicate with a parent process, whether using WebSockets or Pipes.&lt;br /&gt;
The problem is that I was never able to implement pipes on Puppeteer-Sharp, and I really want to implement it on Playwright-Sharp.&lt;br /&gt;
So let’s see how far are we from getting this done.&lt;/p&gt;

&lt;h1 id=&quot;communicating-two-nodejs-processes&quot;&gt;Communicating two Node.JS processes&lt;/h1&gt;

&lt;p&gt;Implementing pipes in Node.JS it’s pretty simple and straight forward.&lt;br /&gt;
Let’s say I have a parent and a child app. When the parent app creates the child app process, it will be able to set up the child’s stdin (File descriptor 0), stdout (File descriptor 1) and stderr (File descriptor 0), but not only that, it will also be able to create more file descriptors, which can be used as an extra set of pipes.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const server = childProcess.spawn(
  &apos;node&apos;,
  [&apos;../child/index.js&apos;],
  {
    stdio : [&apos;inherit&apos;, &apos;inherit&apos;, &apos;inherit&apos;, &apos;pipe&apos;, &apos;pipe&apos;]
  }
);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this call, we are not only setting up stdin, stdout, and stderr, but we are also creating two pipes using the file descriptor 3 and file descriptor 4.&lt;/p&gt;

&lt;p&gt;Let’s create two small scripts to test this. We will write a parent app which will read an input from the terminal, it will send it to the child app, and the child app will echo it.&lt;/p&gt;

&lt;p&gt;The parent app would look something like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const readline = require(&apos;readline&apos;);
const childProcess = require(&apos;child_process&apos;);

// Create child process
const server = childProcess.spawn(
  &apos;node&apos;,
  [&apos;../child/index.js&apos;],
  {
    stdio : [&apos;inherit&apos;, &apos;inherit&apos;, &apos;inherit&apos;, &apos;pipe&apos;, &apos;pipe&apos;]
  }
);

// We will use the File descriptor 3 as a writer and the File descriptor 4 as a reader.
const reader = server.stdio[4];
const writer = server.stdio[3]; 

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

// When we get data on the reader we print it
reader.on(&apos;data&apos;, data =&amp;gt; console.log(data.toString()));

// We are going to send a message to the child
var waitForMessage = function () {
  rl.question(&apos;&apos;, (message) =&amp;gt; {
  
    if(message === &apos;exit&apos;) {
      rl.close();
    }
    writer.write(message + &apos;\n&apos;);
    waitForMessage();
  });
};

waitForMessage(); 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The child app will be super simple:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const readline = require(&apos;readline&apos;);
const fs = require(&apos;fs&apos;);

// We will create an stream reader from the file descriptor 3
const reader = fs.createReadStream(null, {fd: 3});

// And a writer from the file descriptor 4
const writer = fs.createWriteStream(null, {fd: 4});

// When we get a message on the reader we echo it in the writer
reader.on(&apos;data&apos;, data =&amp;gt; writer.write(&apos;echo: &apos; + data + &apos;\n&apos;));

// This is how a .NET developer leaves an app open in Node.JS
setInterval(()=&amp;gt; {}, 1000 * 60 * 60);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So far, so good. It works&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kblok/kblok.github.io/master/img/interprocess-communication/it-works.png&quot; alt=&quot;award&quot; class=&quot;center-image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s get to the important part. Can I connect a .NET app there?&lt;br /&gt;
.&lt;br /&gt;
.&lt;br /&gt;
.&lt;br /&gt;
.&lt;br /&gt;
No, you can’t. It’s not possible to add more streams on a Process instance in C#. You will find a &lt;a href=&quot;https://github.com/dotnet/runtime/issues/26559#issuecomment-399115826&quot;&gt;way better explanation here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, our next and final test would be trying to create a .NET app, create new pipes in there, and then passing those file descriptors as an argument to the child app.&lt;/p&gt;

&lt;p&gt;The child app would be quite simple. We replace the hardkoded descriptors 3 and 4 with command line arguments:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const readline = require(&apos;readline&apos;);
const fs = require(&apos;fs&apos;);
const reader = fs.createReadStream(null, {fd: process.argv[2]});
const writer = fs.createWriteStream(null, {fd: process.argv[3]});

reader.on(&apos;data&apos;, data =&amp;gt; writer.write(&apos;echo: &apos; + data + &apos;\n&apos;));


setInterval(()=&amp;gt; {}, 1000 * 60 * 60);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, let’s go to the .NET world. We know there is something called &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-use-anonymous-pipes-for-local-interprocess-communication?WT.mc_id=DT-MVP-5003814&quot;&gt;Anonimous Pipes&lt;/a&gt;.
Let’s see if that works:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;static async Task Main(string[] args)
{
    // We are going to create two pipes, one writer and one reader.
    using var pipeWriter = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable);
    using var pipeReader = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);

    // We create a child process passing the pipes handles as string.
    Process client = new Process();

    client.StartInfo.FileName = &quot;node&quot;;
    client.StartInfo.Arguments = &quot;../../../../Childwitharguments/index.js &quot; + pipeWriter.GetClientHandleAsString() + &quot; &quot; + pipeReader.GetClientHandleAsString();
    client.StartInfo.UseShellExecute = false;
    client.Start();
   
    // If microsoft docs tells me to call this method I will.
    pipeWriter.DisposeLocalCopyOfClientHandle();
    pipeReader.DisposeLocalCopyOfClientHandle();

    // We start listening to messages
    _ = StartReadingAsync(pipeReader);

    // We create a stream writer, and we will write messages on that stream.
    using var sw = new StreamWriter(pipeWriter)
    {
        AutoFlush = true
    };

    string message = Console.ReadLine();

    while (message != &quot;exit&quot;)
    {
        await sw.WriteAsync(message);
        message = Console.ReadLine();
    }

    client.Close();
}

private static async Task StartReadingAsync(AnonymousPipeServerStream pipeReader)
{
    try
    {
        StreamReader sr = new StreamReader(pipeReader);
        
        // This method should get a CancellationToken so we use that instead of a while true.
        // But this will work now.
        while (true)
        {
            var message = await sr.ReadLineAsync();

            if (message != null)
            {
                Console.WriteLine(message);
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kblok/kblok.github.io/master/img/interprocess-communication/it-works-dotnet.png&quot; alt=&quot;award&quot; class=&quot;center-image&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;final-words&quot;&gt;Final Words&lt;/h1&gt;

&lt;p&gt;It was so cool to find that we can implement this kind of solution between both frameworks.
I know that implementing pipes is not something most of us need in our daily job, but you know, maybe someday you will need to connect to a browser using these pipes 😉.&lt;/p&gt;

&lt;p&gt;If you want to take a look at the source code, you can find &lt;a href=&quot;https://github.com/kblok/DotNetNodeInterprocessCommunication&quot;&gt;the repository on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Don’t stop coding!&lt;/p&gt;
</content>
  </entry>
  
</feed>
