diff --git a/.gitignore b/.gitignore index 62bf7251e6d5b..9fef3eba34682 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ _site .DS_Store +.idea .jekyll .jekyll-metadata .bundle diff --git a/_config.yml b/_config.yml index d4916414195c9..cd7ab638ef24c 100644 --- a/_config.yml +++ b/_config.yml @@ -3,13 +3,13 @@ # # Name of your site (displayed in the header) -name: Your Name +name: Scott Rippey # Short bio or description (displayed in the header) -description: Web Developer from Somewhere +description: Full-Stack Engineer, Visual Designer # URL of your avatar or profile pic (you could use your GitHub profile pic) -avatar: https://raw.githubusercontent.com/barryclark/jekyll-now/master/images/jekyll-logo.png +avatar: https://avatars3.githubusercontent.com/u/430608 # # Flags below are optional @@ -21,27 +21,27 @@ footer-links: email: facebook: flickr: - github: barryclark/jekyll-now + github: scottrippey instagram: - linkedin: + linkedin: scottrippey pinterest: rss: # just type anything here for a working RSS icon - twitter: jekyllrb - stackoverflow: # your stackoverflow profile, e.g. "users/50476/bart-kiers" + twitter: + stackoverflow: users/272072/scott-rippey # your stackoverflow profile, e.g. "users/50476/bart-kiers" youtube: # channel/ or user/ googleplus: # anything in your profile username that comes after plus.google.com/ # Enter your Disqus shortname (not your username) to enable commenting on posts # You can find your shortname on the Settings page of your Disqus account -disqus: +disqus: scottrippeyblog # Enter your Google Analytics web tracking code (e.g. UA-2110908-2) to activate tracking -google_analytics: +google_analytics: UA-177199022-1 # Your website URL (e.g. http://barryclark.github.io or http://www.barryclark.co) # Used for Sitemap.xml and your RSS feed -url: +url: https://scottrippey.github.io/ # If you're hosting your site at a Project repository on GitHub pages # (http://yourusername.github.io/repository-name) diff --git a/_posts/2014-3-3-Hello-World.md b/_posts/2014-3-3-Hello-World.md deleted file mode 100644 index d4665b6d18e9e..0000000000000 --- a/_posts/2014-3-3-Hello-World.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: post -title: You're up and running! ---- - -Next you can update your site name, avatar and other options using the _config.yml file in the root of your repository (shown below). - -![_config.yml]({{ site.baseurl }}/images/config.png) - -The easiest way to make your first post is to edit this one. Go into /_posts/ and update the Hello World markdown file. For more instructions head over to the [Jekyll Now repository](https://github.com/barryclark/jekyll-now) on GitHub. \ No newline at end of file diff --git a/_posts/2020-08-01-my-projects.md b/_posts/2020-08-01-my-projects.md new file mode 100644 index 0000000000000..cd71c83985b7b --- /dev/null +++ b/_posts/2020-08-01-my-projects.md @@ -0,0 +1,79 @@ +--- +layout: post +title: Scott Rippey's Projects +--- + +Here's a showcase of my open source projects, and a few stories about my past work projects. Enjoy! + +## My Open Source projects + +### [SmartFormat.NET](https://github.com/axuno/SmartFormat) +This is a really simple project that I worked on for fun. It's a string-template engine for .NET, which I wrote many years ago. +However, as I moved away from .NET development, a kind user, @axuno, became a major contributor, and eventually took ownership and maintenance of the repo. +To my surprise, it has nearly 1 million downloads in NuGet now! + +### [XQuestJS](http://scottrippey.github.io/xquestjs/) +A fun little "space shooter" game that you can play in your browser, phone, or tablet! + +![xQuest 1 Heat of the Battle](https://user-images.githubusercontent.com/430608/92063687-fade5080-ed58-11ea-89e5-d133f7a72c17.png) + +### [RippeyEats](http://scottrippey.github.io/sample-RippeyEats/) +This was done as a little "interview exercise". It's a nice little demonstration of a React, TypeScript, TailwindCSS, and Parcel development stack, and should showcase some of my coding skills. + +![RippeyEats Sample](https://user-images.githubusercontent.com/430608/91997404-4a8c3000-ecf7-11ea-8068-d63ab8f34a84.gif) + + + +## My Work Projects + +I've written a LOT of code in my career, but as is common, most of it lies in the hands of my employers! Here's a couple of the interesting projects I've worked on, and some stories behind them. + +### [InVision Studio](https://www.invisionapp.com/studio) + +![InVision Studio](https://user-images.githubusercontent.com/430608/92064206-41807a80-ed5a-11ea-8f45-e43354e64938.gif) + +#### The Beginning +I was drawn to InVision by the tagline "made for designers, by designers". I love designers! I actually wish I was one, but my technical skills have always outweighed my artistic skills. Gotta pay the bills! But I've always loved working as the middle-man, between the design team and the engineering team. Like a Front-FRONT-end engineer. +So, I was excited to work on design software. And InVision's designers are awesome ... really the best of the best. After a few months into my employment, I caught wind of a "top secret" project ... something to do with a canvas and animation ... so I had to get in on that! And sure enough, I found myself working on the most interesting project I could think of! Creating an actual Design Tool! + +#### The Project +InVision Studio was my dream project. An opportunity to not only build a design tool, but to master using it, and to even make it better. + +It was all built on web technologies, too, which I loved. That kept me up-to-date with my skill set, and taught me a lot of new things I wanted to learn. React, TypeScript, and Electron are all very interesting technologies, and are skills that are in top demand. I'm happy to have those on my resume! + +#### A Funny Story +When I first started working on Studio, it was a top-secret project. I signed an NDA, and couldn't even talk about the project to most coworkers. +I was in a video call with a few managers, and we were discussing a 3rd party tool that we needed to authorize with our Purchasing department. +So the Purchasing department head, Shalom, asked me to add him to the project. Sharing my screen, I typed his name, autocomplete showed suggestions, and I quickly hit Enter. +He tells me "I still don't have access" ... + +A few quiet seconds go by, as I slowly realize that the "Shalom" I selected was not the "Shalom" on the call. He wasn't even part of our company. +So I had just given some stranger admin access to our top-secret application. With all of my managers watching. + +Flushed, I removed him, and we all kinda chuckled at my mistake. But, how bad could it really be? Some random guy named Shalom will get an email, revealing the fact that InVision was working on something called `studio-app` ... he'll just delete it, and everything will be fine, right? + +Still sharing my screen, I go to Shalom's profile. And he's the lead developer for a product called Pixate. A design tool. A direct competitor. Who now knows that InVision is entering the ring. + +We all kept chuckling, as my manager asked me where they should send my last check. I didn't actually lose my job that day, but I did spend the next week renaming all our repos with codenames, to make sure I didn't leak that info again. And they took away my keys. + + +### Xbox One App for AT&T Uverse + +![ATT Xbox 2 Movies](https://user-images.githubusercontent.com/430608/92063875-77712f00-ed59-11ea-85a5-7fa1cfca5426.jpg) + + +#### The Beginning +I worked for AT&T's Uverse cable service (through a company called Piksel), and worked initially on their website, streaming TV and Movies, a lot like Netflix. +Before the Xbox One was announced, our company was commissioned to develop an Xbox One app for AT&T. I was super excited, to actually develop a media app for a video game console! Plus I felt all special, because this was all "top secret" before the console was released. I called my best friend and bragged that night. + +#### The Project +I got to work with a 50" TV on my desk, and a controller next to my keyboard, so I felt pretty special. The development console didn't actually play games, so I wasn't really that special, but it felt cool! +The app went live, and as a Thank You gift, I received my own Xbox One console. It was a really fun project. + +#### A Funny Story +Well, I was pretty bummed that my development console couldn't play games. However, I was in the middle of developing my own JavaScript game for fun ... so I actually bundled my own video game as an easter egg inside the Uverse App! I didn't want ANYONE to know, since I was afraid that the easter egg would invalidate the Certification process, so I hid it really well. I didn't even test it -- because it was pretty hard to hide a video game on a 50" screen! +One night, I had to work super late, and was the last in the office. I finally used that chance to launch the game, and worked out a few bugs, and that was all the testing I could really do! I successfully shipped the game in the app. + +Unfortunately, last I checked, AT&T has discontinued their Xbox One app support, so my app no longer sees the light of day. At least [the game I wrote](http://scottrippey.github.io/xquestjs/) still lives on! + +[last updated Sept 2, 2020] diff --git a/_posts/2020-08-10-how-to-create-a-testing-culture.md b/_posts/2020-08-10-how-to-create-a-testing-culture.md new file mode 100644 index 0000000000000..d7e454be830f8 --- /dev/null +++ b/_posts/2020-08-10-how-to-create-a-testing-culture.md @@ -0,0 +1,39 @@ +--- +layout: post +title: How To Create a "Testing Culture" +--- + +I love writing unit tests. I evangelize their value to teammates, and I'm always cultivating "testing culture" within my teams. I've developed several projects that have very high coverage (~95%), and those projects are my absolute favorite to work in. + +However, I've also seen many testing attempts fail. And it's always the same reasons: +- Deadlines - not enough time/resources to add tests +- Maintenance - Tests become outdated and obsolete +- Agility - specs aren't know ahead of time, so TDD doesn't really work +- Value - adding tests to existing code doesn't provide much value + +**These failures are due to a common misunderstanding!** There is a core testing concept that developers need to understand: + +## Tests are not a finishing tool. They're a building tool. +If you're not using tests for building, you're missing the vast majority of their value! + +Imagine you're building a **house of cards**. The tests are like "scaffolding" built up, behind the cards, holding things securely in place. +If you try to build the house first, and then add the scaffolding second, you're wasting your time! The house is built; the scaffolding isn't critical. It might add ridgidity, but it becomes a hinderance as the house constantly changes. +Instead, imagine you add the scaffolding as you build each section of the card house. Each section will be secure, and building on top of other sections is stable. The scaffolding holds the little sections securely, and allows you to build faster on top. It allows you to move sections around, as the shape of the house changes. + +So, to cultivate a "testing culture", **the tests need to be a development tool**. In many cases, it could be your PRIMARY development tool! Tests run fast, they can address every use case, they don't require user intervention or following instructions. +So once you've started writing code, make it your top priority to run that code in a test runner. You don't need assertions, you don't need to define all the specs just yet. Just `console.log` something to the screen, so that you now have a **unit-test development environment**. + +When you have a unit-test development environment, the aforementioned problems are solved! +- Deadlines - tests speed up the development process +- Maintenance - tests are updated during development +- Agility - specs are changed when the code changes +- Value - not only do the tests speed up development, but they also provide stability + +When you see a codebase with a high test coverage, I can almost guarantee it's because the tests are the development environment. That's how you maximize the value of tests, and how you cultivate a "testing culture". + +# Real World Examples + +Here are a few of the projects that I worked on that truly benefitted from unit-test development environments. Hopefully these demonstrate the value of the "unit-test development environment". + +1. A deployment workflow chatbot. Manual testing was extremely difficult and time-consuming, due to many interconnected systems. So I mocked them all out, and isolated the chatbot logic. That allowed us to do 95% of our development from within unit tests, and only needed to manually test once ready to go into production. +2. A text-layout algorithm. Manual testing involved running the entire application, getting text into the correct state, and visually inspecting it. Instead, the unit tests rendered the results directly to an image for inspection. This allowed us to develop and test against 100s of scenarios and edge cases. diff --git a/_posts/2020-08-12-why-you-should-stop-using-promises.md b/_posts/2020-08-12-why-you-should-stop-using-promises.md new file mode 100644 index 0000000000000..5ba315506677d --- /dev/null +++ b/_posts/2020-08-12-why-you-should-stop-using-promises.md @@ -0,0 +1,34 @@ +--- +layout: post +title: Why you should STOP using Promises! +--- + +Four years ago, I wrote some articles explaining how and why you should use JavaScript Promises: [Understanding JavaScript Promises](https://engineering.invisionapp.com/post/understanding-promises/) and [8 Tips for Mastering JavaScript Promises](https://engineering.invisionapp.com/post/mastering-promises/). +Well, times have changed, and I'm here to say: you should STOP using Promises! + +## Use async functions instead! + +Let me get straight to the point. For async code, you should be using an `async function` (and its `await` syntax). + +Async functions allow us to use the same **language features** as our sync code, instead of using the Promise APIs. +- `await` instead of `.then` +- `catch` instead of `.catch` +- `return` instead of `Promise.resolve` +- `throw` instead of `Promise.reject` +This results in cleaner, more consistent code. + + +## Promises are just _implementation details_ + +You might say "async functions are just syntax sugar for Promises". But I disagree! I'd argue that **async functions** were actually the end goal from the start. Promises just bridged the gap, as a way to achieve some of the benefits of async programming before the language supported it. Now that we have async function support, we've arrived. Promises got us here, but now they're fading into an _implementation detail_. I like to think of Promises like an async callstack; something you should be aware of, but for all intents and purposes, something you forget about. + + +## Promises are only needed for 2 scenarios + +I think there's only 2 scenarios where you should use a `Promise` in your code. +1. Converting a callback API into Promise API, so it can be `await`ed. For example, you might want to wrap `setTimeout`, or perhaps wrap a library that only supplies a callback API. It is your responsibility, then, to promisify that API, typically using a utility or simply `new Promise(...)`. +2. `Promise.all` is a useful utility that should be used for waiting for results in parallel. + +## Conclusion +The more you write async functions, the less you think about Promises. They fade away into the background, and you get used to the clean, flat structure of `async` code. +So if someone you know uses the Promise syntax, and it makes you cringe, please send them to this article. diff --git a/_posts/2020-09-02-tailwindcss.md b/_posts/2020-09-02-tailwindcss.md new file mode 100644 index 0000000000000..984eb03eab609 --- /dev/null +++ b/_posts/2020-09-02-tailwindcss.md @@ -0,0 +1,111 @@ +--- +layout: post +title: TailwindCSS with React, the pros and cons +--- + +I recently completed an "interview exercise", where I was challenged to create a simple prototype of a web page. The goal was to demonstrate my coding skills, by creating it from scratch. + +It's always fun to start a project from scratch, especially when you get to choose your own software stack! Here's the stack I chose: + +- React +- TypeScript +- TailwindCSS +- Parcel (building / bundling) + + +The result was pretty simple: + +![RippeyEats Sample](https://user-images.githubusercontent.com/430608/91997404-4a8c3000-ecf7-11ea-8068-d63ab8f34a84.gif) + + +## Thoughts on "The Stack" +I loved it! Why else would I choose it? + +**React**, as expected, is awesome ... the code quality is great, I feel like components are easy to reason about, and the code I wrote is modular and could be reused or refactored easily. + +**TypeScript** didn't get in my way, but I really didn't use it much in my UI code. Since there wasn't a lot of non-UI in this project, TS played a small role in my code. + +**Parcel** was awesome to work with -- it seriously just worked! Having come from a long WebPack background, it was refreshing that _almost_ everything **worked out of the box**! I added a few plugins _by simply installing them_! Literally no Parcel configuration needed. And it was fast, and live-reloaded the page. Wonderful! + +**TailwindCSS** was probably the **most interesting** part of this project, and was the only thing that had some cons. I'll talk more about that for the rest of this article. + + +## TailwindCSS + +Having recently worked with TailwindCSS in other projects, I'm glad to have used it on this fresh project. +Overall, I loved it, and would _probably_ choose it again. But it has certain flaws, which I'd like to discuss here too. + +#### Just Utilities, not a framework +- ✅ Tailwind has no preconfigured styles. It's "vanilla" CSS, basically. I added my own `theme` configuration, chose the fonts, colors, and units of measurement. **This is perfect for "pixel perfect" development**. + Tailwind does have some optional "framework" plugins, which would be great for rapid prototyping or custom projects, but since I'm usually going for "pixel perfect" designs, Tailwind was perfect. It gave me total control. +- ⚠️ Tailwind has a LOT of configuration options, which is good, but I did find myself struggling with the defaults. All sizing is done, by default, in `rem` units; I was hoping to easily switch to `px` instead, but it took a lot of config to get to that point. + I ended up "generating" my own config values, like this: + ```javascript + // tailwind.config.js + module.exports = { + // Use px instead of rem: + fontSize: generateSizes(range(10, 40, 2), 'px'), + lineHeight: generateSizes(range(2, 60, 2), 'px'), + spacing: generateSizes(range(-10, 100, 5), 'px'), + // Generate ALL variants, for convenience; it's just too much work to manage a per-prop list: + variants: [ 'responsive', 'group-hover', 'group-focus', 'focus-within', 'first', 'last', 'odd', 'even', 'hover', 'focus', 'active', 'visited', 'disabled' ] + }; + function generateSizes(values, unit) {...} + function range(start, end, skip) {...} + ``` + This handles things like `padding` and `line-height`, but I ended up using inline styles for `height` and `width` (Tailwind just can't / shouldn't generate all h/w values). + Generating all those classes (for the dev build) initially takes pretty long (like 8s), but Parcel does an excellent job caching, so all other changes, including CSS, are near-instant. + + +#### CSS-in-JS + +- ✅ I **love** having my CSS right there with my HTML. They go together! CSS-in-JS was a great part of the development process; fast, efficient, same file, closely coupled (a good thing!). +- ✅ The thing I really loved about Tailwind, over something like `styled-components` or `css-modules`, is the fact that CSS can be expressed so tersely. **Basically, it's just a super-abbreviated CSS syntax**. For example, here's an HTML element AND all the CSS defined in just 1 line of code: + ```html + + ``` + The CSS is "collapsed" into this one little line, so it doesn't really break the flow of the document. +- ⚠️ However, that's probably the **biggest problem** too. + **Reading** those classes, you can probably guess what they do, and it makes sense. + But **writing** those classes -- it was hard! I often knew the CSS I wanted to write, but had to use the docs to look up each class pattern. Even with an IDE plugin installed, it was really hard to get the class names right. And the worst part, if they're wrong, or misspelled, or a value that's out-of-range, it silently fails. +- ⚠️ The Tailwind docs are very good, but the docs are so COMPLETE that it takes a while to find the right page. I wish they had an official 1-page cheat-sheet, so I could just search for the CSS I needed, and find the class pattern. + + +#### Pseudo classes, responsive classes + +- ✅ Pseudo selectors, like `:hover` or `:first-child`, can't be implemented by inline styles. So that's a huge advantage of Tailwind; the `hover:bg-blue` class can be added inline! +- ✅ Responsive design was REALLY easy, with classes like `lg:hidden`! Tailwind, by default, uses `min-width` breakpoints, which encourages "mobile-first" design. + + +#### Tiny size + +- ✅ Tailwind CSS is designed to be compiled, and makes it SUPER easy to add PurgeCSS to the build. So this means, with very little effort, my ENTIRE CSS bundle is tiny at 20kb (15kb of `normalize.css`). + I even added a huge selection of size configurations, which bloated the dev bundle size, but was purged from the final build! + + +#### Semantics + +- ⚠️ The second biggest problem I had with TailwindCSS: some of my code suffered from poor semantics. + For example, several elements required padding of 20px, and sometimes they'd have a negative margin of equal value. + Normally, I'd love to define a variable like `@pad-x: 20px`, and then use the variable to write understandable semantic code: + ```less + @space-between-elements: 20px; + div { + padding: 0 @{space-between-elements}; + margin: 0 -@{space-between-elements * 2}; + height: 20px; /* (value is unrelated to @space-between-elements) */ + } + ``` + However, with Tailwind, I ended up with just "magic numbers" in my classes, like: + ```jsx +
...
+ ``` + This is shorter, obviously, but if I want to change the space between elements, I have to do a lot of work to figure out where and how it was being used. I might accidentally change `h-20`, or I might forget to change `-mx-40`. + I considered a JavaScript variable for these values, but that breaks the PurgeCSS requirement of no class interpolation, so I went with the magic-numbers approach. + I'm bummed, though, because CSS is hard enough, and I really love using variables to improve the semantics. This might even be a deal-breaker for me in the future, when working on much-more-complex UIs. + + +## Conclusion + +Like I said earlier, I'd be happy to use TailwindCSS on another project! But considering the few problems, I'd be eager to try `styled-components` or `css-modules` next, so I could compare the differences. +But the rest of this stack was solid, and I felt like I had a great development experience, high velocity, and was able to implement even the harder parts with ease. diff --git a/_posts/2022-05-23-adopting-typescript-in-phases.md b/_posts/2022-05-23-adopting-typescript-in-phases.md new file mode 100644 index 0000000000000..ea9db3defd020 --- /dev/null +++ b/_posts/2022-05-23-adopting-typescript-in-phases.md @@ -0,0 +1,215 @@ +# Build, Migrate, Improve: A Three-Phase Approach to Adopting TypeScript + +> Originally published at https://nearform.com/digital-community/adopting-typescript-in-phases/ + +![Photo By Mike Hindle of a typewriter sitting on a desk](https://res.cloudinary.com/formidablelabs/image/upload/f_auto,q_auto/v1675121564/dotcom/uploads-mike-hindle-dwyrc2jumgs-unsplash "Photo By Mike Hindle") + +At Formidable, we've converted many of our OSS projects to TypeScript, including [Spectacle](https://github.com/FormidableLabs/spectacle/pull/1075) and [react-swipeable](https://formidable.com/blog/2020/react-swipeable-ts/), and have helped many of our clients adopt TypeScript as well. + +Converting a project to TypeScript is an investment worth making. Here’s what you need to know to make the process go smoothly and quickly. + +## The Three-Phase Approach + +Our best strategy for adopting TypeScript is to break it down into three phases: **build**, **migrate**, and **improve**. + +* In the **build** phase, we focus only on the configuration required to build TypeScript files. +* In the **migrate** phase, we focus on converting JS files to TS, while trying to keep the code as intact as possible. +* In the **improve** phase, we focus on making code improvements: improving types, adding stricter rules, and so on. + +This phased approach reduces the complexity of each change and reduces conflicts along the way. If your project is large or has multiple contributors, it's especially important to merge after each phase! + +## Before we start: why bother? + +Before we start, we need to identify our motivation for adding TypeScript to our project. Our reasons for migrating an **existing project** to TypeScript might not be the same as the reasons for choosing TypeScript for a **new project.** "Preventing bugs" and "increasing velocity" aren't as relevant when the project already exists! + +Let’s focus on the goals that make the most sense when converting a project: + +* **Strengthen our existing code** + Strong types are an important part of the “[Testing Trophy](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications)”, and adding TypeScript types to your code is like adding unit tests. They add strength and stability to the codebase and give you confidence in making changes. +* **Make our APIs easier to consume** + Publishing our types with our libraries makes them far easier to consume, for JS and TS consumers alike. +* **Find bugs** + As you convert files to TypeScript, it's likely you'll start spotting type issues that could be causing bugs. +* **Improve future development** + TypeScript vastly improves the IDE experience: auto-completion, code navigation, detecting type errors, etc. + +## Phase 1: Build + +The goal of this first phase is to get a single, simple `.ts` file to compile! + +### Create your `tsconfig.json` file + +The best way to initialize your project for TypeScript is to run these two commands: + +```sh +npm install --save-dev typescript +npx tsc --init +``` + +The first command installs TypeScript and the `tsc` compiler. The second command generates a fully-annotated `tsconfig.json` file for you. The “fully annotated” file is especially useful because it includes detailed explanations for EVERY option! The generated file is also going to be more up-to-date than most examples you find on the internet. + +### Relax your TS config + +Typically, TypeScript's `strict` mode is our friend. It ensures our code is very-strongly-typed and forces a lot of great patterns. + +However, our initial goal of conversion is to get our JavaScript files to compile as TypeScript. To do this, we need to really relax our TS config! + +In your `tsconfig.json` file, change `"strict"` to `false`. Look through all the other “type checking” options, and turn them off for now. We’ll turn these options back on eventually in **phase three** but for now the more relaxed, the better! + +Apart from changing `“strict”`, the rest of the generated options are good. I especially want to mention `"esModuleInterop"`, which should be set to `true` . This allows `import foo from 'foo';` (or `const foo = require('foo');`) to continue working. + +### Configure your build tool + +Configuring the various build tools is a bit out of scope for this article, but I wanted to give you an idea of what this will take. + +Some build tools both **compile** and **type-check** at the same time. But this approach is slow and cumbersome. It’s much faster to split these jobs into separate tasks. + +You should run `tsc --noEmit` on your project to perform all the type-checking. I highly recommend doing this in a CI job. And typically, your code editor will highlight your TypeScript errors as you develop, so you probably don’t need to run this manually often. + +All that’s left is configuring your build tool to “strip out” the TypeScript code! + +* **webpack** - + Be sure to disable type-checking with `transpileOnly: true` (as mentioned above). +* **babel** - +* **Rollup.js** - +* **[esbuild](https://esbuild.github.io/content-types/#typescript), [parcel](https://parceljs.org/languages/typescript/), [snowpack](https://www.snowpack.dev/reference/supported-files#typescript), [storybook](https://storybook.js.org/docs/react/configure/typescript)** - These tools automatically compile TypeScript out of the box! + +Here’s a list of other tools you might be using, which will need to be configured to load TypeScript too: + +* **eslint** - +* **jest** - +* **karma** - +* **Others** - many tools, including Node itself, accept a `--require` parameter, which can be used with `ts-node/register` to compile TypeScript. Eg. `node --require ts-node/register ./my-script.ts` . This can be used for many tools that otherwise don’t support TypeScript. + +### Compile your first TypeScript file + +The goal of this whole phase is to get *something* to compile ... just one simple little file is all we need for now! You can find a simple utility in your codebase and convert it, or even just create a `hello-typescript.ts` and get it to compile. + +Once we have this working ... it’s time to merge, and start phase two! + +## Phase 2: Migrate + +In this phase, we will focus on migrating our JavaScript code into the TypeScript ecosystem. Our goal is to make the minimum changes to our code and get a working build. + +### Automatic Migration Tools + +There are tools that can help convert your `.js` files to `.ts` and will save a ton of time. These tools give you a great head start, but can only go so far; they use a lot of `any` and `// @ts-ignore` (which is OK for this phase). + +* [ts-migrate](https://github.com/airbnb/ts-migrate) - Migrates `.js` files to `.ts`, and adds some TypeScript annotations along the way. + Docs: +* [ratchet](https://github.com/mskelton/ratchet) - Converts from `React.PropTypes` to TypeScript types + +### Use `any` when blocked + +Remember, the goal of converting to TypeScript is to **progressively** improve code quality. We don’t have to achieve "100% type-coverage" right away. + +So when there's something that's difficult to type correctly, feel free to use `any`. This just tells TypeScript "don't worry, this works" and will help speed along the conversion. + +After all, your entire JS library was typed as `any` to start with ... there's no harm in leaving this improvement for a later time. + +### Use `// @ts-ignore` sparingly! + +The problem with `// @ts-ignore` is that the entire line will be ignored. It can mask multiple errors, even if it was originally used to mask a specific error. + +It's better to use a cast in these situations so that you bypass only a single error. For example, use `as any` or better yet `as unknown as SomeType`. + +However, on occasion, there’s a problem that’s too difficult to correctly type, or an `import` statement that just can’t be appeased, so that’s an OK time for ignoring the line.\ +You can also consider using `// @ts-expect-error`, which is almost identical — except if you ever do **fix** the underlying error, then this will remind you to remove the comment! + +### Minimize Git diffs and conflicts + +If your project has multiple contributors or multiple branches, then this one is important! + +When you **rename** your files from `.js` to `.ts`, and **more than 50%** of the file has differences*, then Git will treat it as a **delete and create** (instead of a **rename**). + +This causes a TON of headaches! Difficult merge conflicts. Terrible PR diffs. Here’s a small example of a file that crossed the 50% threshold: + +![Bad diff from Spectacle](https://res.cloudinary.com/formidablelabs/image/upload/f_auto,q_auto/v1675121564/dotcom/uploads-spectacle_ts_conversion) + +*Source: [the Spectacle TS conversion](https://github.com/FormidableLabs/spectacle/pull/1075/files#diff-4854a061f4941a6e33dc611c304cde30679478f448a9fdeb4a30b10df5dc78d5)* + +To minimize this problem, you have to try to stay under that 50% threshold*. Rename the file; don't reformat your code, don't indent lines, don't refactor, if possible! Simply add the types that are necessary, and commit the minimum. It can be quite difficult, especially with smaller files, to stay under that 50% threshold. + +> **\* Note: How does Git measure a "50% difference"?** +> This is measured by **lines changed**. So if you simply indented 20 lines of code, and the file was only 40 lines long, you'd hit that threshold! The diff would show a delete and create instead of a rename. +> The diff is cumulative, too ... so if you change too much, you can actually add commits to reduce the diff. + +### Hold off on fixing bugs + +As you convert files, it's possible that you'll find issues that you want to fix. +Things like "this should be a string" or "this function is missing an argument" will pop up, and you'll probably want to fix it! + +Before you fix any potential bugs, consider: + +* Is fixing this a "scope creep"? +* How much extra work will this be? (eg. adding unit tests, documentation?) +* Could fixing this be risky? + +You might want to consider dropping in a `// TODO` comment for now and fixing this SEPARATELY from the TS conversion. That way, the TS conversion won't be hindered by scope creep or regressions. + +As an example, when converting Spectacle, I saw a `return;` which I assumed could be improved to `return null;` but sure enough, it ended up [causing a regression](https://github.com/FormidableLabs/spectacle/pull/1098#pullrequestreview-898237272)! + +## Phase 3: Improve + +Once all your files have been migrated to TypeScript, you can start with the fun stuff: improving your “type-coverage”. + +This phase is where you should add stronger types, install type definitions for dependencies, enable TypeScript's `strict` options, and remove `any`'s. + +Most of these improvements can be made progressively, like removing an `any` whenever you see it. Some improvements need to be fixed project-wide, like when you enable a `strict` check. + +It’s really up to you and your team to decide how aggressively you want to increase your type-coverage. + +### Install third-party types for your dependencies + +In an ideal world, all your dependencies will have strong type definitions bundled! +But unfortunately, many will not have types bundled, and you'll have to add them with one of these three methods: + +* Install a `@types` package for the dependency. For example, if you have a dependency on `"react": "^18"`, you should run `npm install --save-dev @types/react@18`. If not bundled with types, the most popular libraries will have a corresponding `@types` package. +* Upgrade the package, because newer packages might have bundled types. But be warned: upgrading packages can cause regressions, especially if upgrading to a major version that now includes types. +* If the types are still not available, you'll have to manually create your own types. Typically, these go in a `./types/{module-name}.d.ts` file. Typing an external library can be very challenging; use `any` if you get blocked. + +### Enable Strict Mode once you’re ready + +Once you’ve migrated everything to TypeScript, you should try to enable [some strict checks](https://www.typescriptlang.org/tsconfig#strict). Don’t go straight for `"strict": true` and expect your build to succeed! It’s best to enable strict checks one-by-one, and see if your code still compiles. + +The more strict checks you can enable, the better! Some checks might not require any code changes at all! Some might be a disaster, causing hundreds of build issues. + +Here’s what to do when you enable a rule that uncovers a lot of errors: + +* Disable the rule again, and slowly back away (no judgement here!) +* Fix the errors (which could be very difficult and time-consuming) +* Add `// @ts-ignore` or `// @ts-expect-error` comments to the errors. + + * The advantage of this approach: all **new development** will be subject to the strict rules, and all the **old code** will be marked for improvement. + * The `ts-migrate` tool (mentioned above) has a `reignore` feature that can insert these comments for you, too! + +These are the two most useful strict options that are worth enabling (but will likely require fixes): + +* Enable `strictNulls`, and check your code for null-safety. You might need to add the not-null assertion (`!`) some places, which is similar to adding `any` ... it doesn't change the code, it just bypasses the check. + It's better for these explicit `!` assertions to appear in your code, rather than being ignored by the compiler; it makes it easier to spot potential problems. +* Enable `noImplicitAny`, and add explicit types where missing (even if just `any` for now). Again, this makes it easier to spot errors in your code, and those `any`'s act like TODO’s. + +### Add `@typescript-eslint` + +This goes hand-in-hand with enabling strict-mode checks ... linting your TypeScript code adds to the strength and quality of your codebase! It helps you avoid bad TypeScript patterns, and can fix many of them for you too. + +Approach this in a similar fashion to adding TypeScript — be relaxed at first, and progressively enable more linting rules as you fix the problems. + +* Start off with the recommended rules: **[Linting your TypeScript Codebase](https://typescript-eslint.io/docs/linting/)** +* Be sure to read the docs about **[Linting with Type Information](https://typescript-eslint.io/docs/linting/type-linting)** — these rules are **incredibly useful**! But since they run slower and require extra config, they’re not enabled by default. + +### Remove `any`s from your code + +So far, I've encouraged using `any` throughout your codebase; it's a great way to migrate JavaScript into TypeScript land. + +But those `any`s are basically just `TODO`s, and should be improved to real types when possible. You might want to make these improvements progressively, any time you touch a file. But you can also go on an `any` hunt! + +Similarly, keep your eye out for `// @ts-ignore` and `// @ts-expect-error` and remove those when you can too. + +### Publish libraries with at least a minor bump + +This only applies to libraries, not applications, but once you’re happy with the TypeScript conversion, it’s time to publish! + +If your API has changed (even just by adding stricter types), you should publish with a **major** bump. + +But if you've minimized the number of changes to your project, and your API hasn't changed, it's reasonable to publish with a **minor** bump. diff --git a/_posts/2023-02-20-mock-factories-make-better-tests.md b/_posts/2023-02-20-mock-factories-make-better-tests.md new file mode 100644 index 0000000000000..cc07d292876ec --- /dev/null +++ b/_posts/2023-02-20-mock-factories-make-better-tests.md @@ -0,0 +1,195 @@ +# Mock factories make better tests + +> Originally published at https://nearform.com/digital-community/mock-factories-make-better-tests/ + + +![The inside of an old factory](https://res.cloudinary.com/formidablelabs/image/upload/q_auto,f_auto,w_1500/dotcom/mock-factory.png) + +**Developers need a good way to produce mock data. Here’s how we implemented a mock factory for an e-commerce site that’s simple, flexible, and fun.** + +As developers, we use many different tools to test our software. Development servers, component playgrounds (like Storybook), and automated tests (like unit, integration, end-to-end, visual, performance). And there's one thing that all these tools have in common: **they need data**. Which means, as the developer, **you** need data. + +However, **real data** can be difficult to work with. It might not be available, it might be slow, it might not cover edge-cases, and it might not yet have new features. + +Having a good way to produce **mock data** makes all aspects of development easier! A mock factory makes it easy to generate mock data, which results in a project that's easier to develop and test. + +In this article, I will share how we created [this mock factory for an e-commerce site](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/main/packages/nextjs/mocks/factory/index.ts). The end result is simple, powerful, flexible, and fun to use! + +## Where do we need mock data? + +Everywhere. Seriously. + +**Automated tests** is probably the first scenario most people think of. Unit tests, integration tests, visual tests, performance tests, and end-to-end tests all need data. A reusable mock factory will help every aspect of automated testing. + +**Component playgrounds**, like Storybook, also benefit greatly from having realistic mock data to display, so that components can be shown in their full glory. + +**Development servers** can use mock data too. I’ve been on projects where the frontend is working far ahead of the backend, so we used a mock server for development. Having a semi-realistic mock factory helped us maintain a high velocity without getting blocked! + +You might even need to **seed a test database**. A mock factory is perfect for generating a ton of data. + +The beauty is: a single mock factory implementation can be used for ALL these use-cases! + +## What makes a good Mock Factory? + +First I’m going to share our end result, and later I'll share our implementation details. + +### Getting data is incredibly easy + +Need a mock `Product` or a mock `Category`? *Easier done than said*! + +```ts +const product = mock.product({}); +// Returns an object like { id, name, image, price, etc... } +const category = mock.category({}) +// Returns an object like { id, name, image, products, etc... } +``` + +These methods return fully hydrated, realistic data objects, without any effort. All fields have reasonable default values. + +### We provide our own values when needed + +We override the default values by providing our own: + +```tsx +const product = mock.product({ price: 5.99 }); +render(); +expect(screen.findByText("$5.99")).toBeInTheDocument(); +``` + +This way, our tests do not rely on default mock values. Instead, we use overrides to specify ************what matters************ for each scenario. Clearly the `price` matters for this test. + +This results in tests that are easier to understand, and resilient to change. + +### We use randomized data + +Real-world data is hard to predict. Fields can be missing, arrays can be extremely large, strings can be long or short. If you hard-code your mock values, you probably won’t encounter many edge-case scenarios. + +For our mocks, we randomize the default values, which helps us find more edge-cases during development and testing. Here’s an example, using some of the excellent randomization helpers from Faker: + +```tsx +// Price is random: +const msrp = faker.datatype.number({ min: 2, max: 99 }); +// 20% of items are on sale: +const price = faker.helpers.maybe(() => faker.datatype.number({ min: 2, max: msrp }), { probability: 0.2 }) || msrp; +// Choose a random size: +const size = faker.helpers.arrayElement([ 'x-small', 'small', 'medium', 'large', 'x-large' ]); +// Could be null: +const description = faker.helpers.maybe(() => faker.lorem.paragraph(), { probability: 0.8 }) || null; +``` + +### Our randomization has a constant seed + +Using randomized data has a fundamental flaw: the tests need to be CONSISTENT too! + +What if we write a test that *accidentally* depends on randomized data? We might get lucky, and it passes a few times, so we merge. But CI randomly starts failing, and we can't figure out why, and can't reproduce locally (within a few tries) ... what a nightmare. + +Fortunately, this can all be fixed with 1 line of code! Since we use Faker for all our randomization, we simply need: + +```tsx +faker.seed(0); +``` + +By setting the random seed, `faker` will always generate the *same* randomized data each run! +So, if our "accidentally-depends-on-randomized-data" test fails, it should consistently fail, both locally and in CI. This helps us identify and fix the problem, and ultimately end up with a higher-quality test. + +## Our Mock Factory implementation + +Fortunately, creating a mock factory, with all the above features, is really easy, and kinda fun! + +For our small application, we created a single `MockFactory` class in vanilla JS (TS), with methods for creating each of our various data types. An example: + +```tsx +class MockFactory { + product(data: Partial): Product { + return { + id: faker.datatype.uuid(), + name: faker.commerce.productName(), + price: faker.datatype.number({ min: 5, max: 99, precision: 2 }), + images: [this.productImage({})], + ...data, + }; + } + productImage(data: Partial): ProductImage { + return { + id: faker.datatype.uuid(), + url: faker.image.food(), + ...data, + }; + } + // ... more factory methods ... // +} +// Export this as a singleton: +export const mock = new MockFactory(); + +``` + +For larger applications, we'd probably scale by splitting the factory into multiple classes, but the idea would remain the same. + +### Uses Faker to generate interesting data + +This is the fun part! + +Faker provides [tons of categories of fake data](https://fakerjs.dev/api/). It gives our mocks personality and flair, and makes it really easy to create semi-realistic (albeit silly) mockups! + +```tsx +import { faker } from '@faker-js/faker'; + +faker.commerce.productName() +// Returns silly names like "Awesome Rubber Fish", "Practical Granite Gloves", and "Ergonomic Cotton Salad" +faker.hacker.phrase() +// "If we override the card, we can get to the HDD feed through the back-end HDD sensor!" +``` + +> 💡 Be sure to use the correct Faker package: `@faker-js/faker` … the original project [ended in controversy](https://stackoverflow.com/a/70649268/272072). + +### Leaning on Strong Types + +All of our methods have a similar signature, that accepts a `Partial` and returns a complete `TData`. + +To ensure that we don’t forget to mock any optional fields:, we also [use a `Complete` helper](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/c9f3cac26a06aed03a915c0102068b80a4c9c7f4/packages/nextjs/mocks/factory/index.ts#L250-L252): + +```tsx +product(data: Partial): Product { + const result: Complete = { + // All fields are required: + description: faker.helpers.maybe(() => faker.lorem.paragraph(), { probability: 0.8 }), + ...data, + }; + return result; +} +``` + +In this project, most of our types are generated from our GraphQL queries, so it’s wonderfully easy to update a query and get “notified” (by TypeScript) that our mocks need an update too. + +### Nested data is composable + +The `productImage` method could have easily been inlined, especially since it feels very specific to the `product` data. + +However, by exposing it as a separate method, it becomes easier to build overrides: + +```tsx +const productWithTwoImages = mock.product({ + images: [ mock.productImage({}), mock.productImage({}) ] +}); +``` + +We expose the factory methods for most nested types, so that it's really easy to compose new mocks with overrides. + +### Much More + +Our full implementation can be found [in our `nextjs-sanity-fe` repository](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/main/packages/nextjs/mocks/factory/index.ts). + +Take a look to see how we solved: + +- [Circular references](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/c9f3cac26a06aed03a915c0102068b80a4c9c7f4/packages/nextjs/mocks/factory/index.ts#L85) +- [Sequential ID generation](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/c9f3cac26a06aed03a915c0102068b80a4c9c7f4/packages/nextjs/mocks/factory/index.ts#L50-L54) +- [Generating arrays](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/c9f3cac26a06aed03a915c0102068b80a4c9c7f4/packages/nextjs/mocks/factory/index.ts#L29-L33) +- [Ensuring random elements are unique](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/c9f3cac26a06aed03a915c0102068b80a4c9c7f4/packages/nextjs/mocks/factory/index.ts#L35-L47) + +### Alternative Approaches + +Our approach was pretty simple, and relies on vanilla JavaScript with a sprinkle of Faker. + +An honorable mention goes to `@mswjs/data`, which shares most of features that we've built. Its main addition, however, is that it provides a way to query and mutate the data too. This obviously pairs very well with `msw`, and lets you build a mock GraphQL or mock REST server very easily. But that’s a topic for another article! + + diff --git a/_posts/2023-10-13-unlocking-the-power-of-storybook--.md b/_posts/2023-10-13-unlocking-the-power-of-storybook--.md new file mode 100644 index 0000000000000..cf92cf51a2dfb --- /dev/null +++ b/_posts/2023-10-13-unlocking-the-power-of-storybook--.md @@ -0,0 +1,370 @@ +# Unlocking the Power of Storybook + +Going beyond the Design System: how to use Storybook to develop, test, and validate all parts of a frontend application. + +> Originally published at https://nearform.com/digital-community/unlocking-the-power-of-storybook/ + +### Storybook: more than a showcase + +Storybook is a popular tool for frontend web development, best known as a **showcase** for Design Systems and UI Libraries. However, Storybook has features that are far more powerful than simply showcasing UI components! + +Storybook is an excellent **development environment** and **unit testing framework** for most parts of a frontend web application. + +- Here at Formidable, we use it for developing **complex UI components**, that perform data fetching and state management. +- We use it for **testing hooks** in a real browser. +- We use it for **visually testing** our CSS styles and breakpoints. + +Follow along as I show you how we integrate it into our workflow! + +## What Formidable does + +Formidable, a NearForm company, builds large, scalable applications for enterprise clients. + +We recently implemented a Storybook integration for a large client's e-commerce website, and the results were fantastic. We achieved very high code coverage, increased developer velocity, and made the codebase easier to explore. + +For the sake of this article, we will show code from a fictitious, [open-source e-commerce website](https://github.com/FormidableLabs/nextjs-sanity-fe/tree/main/packages/nextjs), "Formidable Boulangerie." This is a website built with React and NextJS, but this article applies to all frameworks compatible with Storybook (React, Vue, Angular, Web Components, etc). + +![alt text here](https://res.cloudinary.com/formidablelabs/image/upload/f_auto,q_auto/v1697129616/dotcom/Marketing%20Content/Screen_Shot_2023-09-13_at_2.11.43_PM.png?w=3116&h=1932) + +Full examples of this code can be found at [FormidableLabs/nextjs-sanity-fe](https://github.com/FormidableLabs/nextjs-sanity-fe/tree/storybook-blog-v1/packages/nextjs) + +## Using Storybook for Developement + +Storybook is an excellent development environment for components. + +- You use multiple stories to render components in all their various states. +- You interact with components in your browser, and manually test them. +- You can easily inspect elements and debug the code. +- You can do all of this in multiple browsers. + +Installing Storybook is very easy, and [their guide](https://storybook.js.org/docs/react/get-started/install) gets you running in a few minutes. + +However, there's still one big hurdle to overcome. Storybook renders components "in isolation", but in a real application, components are rarely isolated! They usually have a lot of dependencies, like Contexts, CSS, framework configurations, and 3rd-party scripts. + +The hardest part of integrating Storybook with an application is providing and managing these dependencies. Below are some of the strategies we used to handle them. + +## How to manage UI Dependencies + +Applications are complex, and our components usually have a lot of dependencies. Even a simple Button will have *at least* CSS dependencies. To render most of our components, we need a way to provide those dependencies. + +We used a global `` component to supply the dependencies, and Storybook's "Decorators” and “Parameters" to customize the Test Harness on a per-story basis. + +### Create a Test Harness + +Most React applications tend to have dozens of nested `Providers` at the root. Most components require many of these dependencies, so Storybook needs to provide them too. + +In order to create stories for our components, we created a global `` wrapper, which provides most of the dependencies that our root application provides. This includes: + +- Top-level framework providers (eg. `react-router`, `framer-motion`) +- I18N Providers (eg. `react-i18next`) +- Common Styles (eg. `global.css`, `tailwind.css`) +- Global State (eg. a ``) +- 3rd-party script mocks (eg. Google Analytics) + +For example, here we add a global `MemoryRouter` and a custom `CartContext.Provider`: + +```tsx +// .storybook/decorators/TestHarness.tsx +import { MemoryRouter } from "react-router"; +import { CartContext, emptyCart } from "~/components/CartContext"; +import "~/styles/global.css"; + +// 👇 Wrapper with all the App's required Providers, along with default values: +export const TestHarness = ({ children, route = "/", cart = {} }) => { + return ( + + + {children} + + + ); +}; +``` + +In a larger application, where the root could have dozens of providers, the `` will likely need the same. + +> Full example: [~/.storybook/decorators/TestHarness.tsx](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/storybook-blog-v1/packages/nextjs/.storybook/decorators/TestHarness.tsx) +> + +### Use Decorators and Parameters + +In Storybook, a **Decorator** is a wrapper around a story. We can add our `TestHarness` globally by adding it to our `.storybook/preview.ts` file like so: + +```tsx +// ~/.storybook/preview.ts +export const decorators = [ + (Story, ctx) => +]; +``` + +> Full example: [~/.storybook/preview.ts#L16](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/storybook-blog-v1/packages/nextjs/.storybook/preview.ts#L16-L19) +> + +Stories pass arguments to decorators via **parameters**. In the decorator above, we’re spreading all `ctx.parameters` as props to the `TestHarness`. So now, our stories can override the default `route` or `cart` by supplying these parameters: + +```tsx +// ~/components/Checkout.stories.tsx +export const WithLargeCart: Story = { + parameters: { + route: '/checkout', + cart: { + items: Array.from({ length: 10 }, () => ({ name: 'cart item' })), + total: 100, + }, + }, +}; +``` + +### Mocking Third Party scripts + +Decorators are also a great way to mock dependencies, like 3rd-party scripts. For example, a component that relies on Google Analytics could inject a `window.dataLayer` object like so: + +```tsx +// .storybook/preview.ts +export const decorators = [ + (Story, ctx) => { + window.dataLayer = []; + return ; + } +]; +``` + +This decorator runs before each story loads, so it’s an easy way to ensure dependencies are set up correctly. + +### Using Storybook for Testing + +Using Storybook as a development environment makes it really easy to **manually** test features. Wouldn't it be great if we could **automate** these manual tests? + +**[Storybook Interaction Testing](https://storybook.js.org/addons/@storybook/addon-interactions)** is the perfect way to do this! It lets you take your existing stories, automate the interactions, and make assertions! + +Storybook added [Interaction Testing in version 7.0](https://storybook.js.org/blog/interaction-testing-with-storybook/), so it's still relatively new. However, it uses familiar tools (Jest's assertions, Testing Library selectors, Playwright runtime), so it's a solid environment with a shallow learning curve! + +## Benefits of Storybook Tests versus headless unit tests + +When compared to Jest (or other headless unit testing tools), Storybook offers many major advantages: + +- There’s no need to repeat the work of mocking a test environment, mounting components, or setting up different variants. +- You can visually see the UI that's being tested. +- You can use the browser's debugging tools, like the console, `debugger` statements, element inspector, network inspector, React Developer Tools, etc. +- Media queries work correctly, and you can test various CSS breakpoints. +- Browser APIs, like `IntersectionObserver` or `matchMedia`, work correctly too. +- You can easily test across multiple browsers. +- You can integrate visual regression testing with VERY little effort. + +### How it works: the `play` function + +Every story can have an optional `play` function, and this is where the magic happens! +The play function has 2 purposes: + +- It interacts with the story; getting the UI into a certain state. + (eg. it fills out a form, or expands a menu) +- It makes assertions. + (eg. it asserts a form validation error has the correct text, or expects menu items to be visible) + +The `play` function runs immediately when the story is loaded, so you can actually see it running. + +Here's a sample story for our `Search` component. It fills in a search term, and validates that search results are shown: + +```tsx +// ~/components/Search.stories.tsx +export const WithSearchTerm: Story = { + async play({ canvasElement, step }) { + const ui = wrap(canvasElement); // The `wrap` function is defined later in this article + + await step("type 'baguette' into the search box", async () => { + ui.searchbox.focus(); + await userEvent.type(ui.searchbox, "baguette"); + }); + + await step("expect to see a loading indicator", async () => { + expect(ui.resultsBox).toBeVisible(); + expect(ui.resultsBox).toHaveTextContent("Loading..."); + expect(ui.resultItems).toHaveLength(1); + }); + + await step("expect to see some search results", async () => { + // Search results are loaded after a short network delay, so we need to wait: + await waitFor(() => { + expect(ui.resultItems.length).toBeGreaterThanOrEqual(2); + }); + }); + }, +}; +``` + +> Full example: [~/components/Search.stories.tsx#L26](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/storybook-blog-v1/packages/nextjs/components/Search.stories.tsx#L26) +> + +When you load the story, the test runs quickly, and the results are shown immediately: + + + +### The Debugging Experience + +Since the Stories and the tests all run inside your browser, the debugging experience is excellent. You simply use all the browser's debugging tools that you're already familiar with. You can set breakpoints, inspect the DOM, manipulate CSS, and even use developer extensions, like React Developer Tools. + +### Visual Regression Testing + +Most of our automated tests validate business logic; is an element visible, does it contain the correct text, does it react correctly to user interactions, etc. While these tests focus on our JavaScript and HTML logic, they ignore a large part of our codebase: the CSS. + +CSS is a huge, fragile part of our application, and it deserves thorough testing. + +Yet CSS is really hard to test! CSS is just the **implementation detail** for implementing a **visual appearance and layout**. Ideally, we want to ignore the implementation, and just validate the appearance, but we can't validate appearance by writing assertions. + +The best way to validate CSS is via Visual Regression testing -- aka "screenshots". If a picture says a thousand words, then **a screenshot makes a thousand assertions**. A single screenshot validates so many things simultaneously: + +- Layout, alignment, responsiveness +- Text content, size, color, weight, transformation +- Image loading, scale + +Storybook is the perfect environment for capturing these screenshots. The components are isolated, consistent, and represent many variations. And best of all, with a little configuration and almost NO code, you can add screenshot tests to EVERY SINGLE story in your application. Imagine, every Story you create comes with dozens of assertions AUTOMATICALLY. It's a wonderful feeling! + +### Chromatic provides the perfect workflow + +[Chromatic is Storybook's paid product](https://www.chromatic.com/docs/#what-is-chromatic), and is by far the best way to achieve Visual Regression Testing. This is not an ad; we're just a big fan of this product. We use it on many of our OSS projects at Formidable, and it provides a lot of great features. It has a generous free tier, and is [straightforward to integrate into CI](https://github.com/FormidableLabs/nextjs-sanity-fe/commit/c14b6e477ccc072a5039bf7cdcd2476feb9ab9a7). + +Its biggest, unique value is how it enables a very smooth workflow for Visual Regression Tests. Here's our typical workflow: + +- A developer creates a Pull Request, which contains UI changes. +- Chromatic automatically captures a screenshot of each Story, and compares it against a baseline. +- If there are differences, the PR gets blocked, awaiting Visual Review for any UI changes. +- Chromatic provides the Visual Review interface, where you compare the differences and approve or deny each one. You can comment on changes, and the interface is easy to use. +- Chromatic even hosts your Storybook, so you can open the stories yourself without running anything locally. +- If all changes are approved, the PR gets unblocked. +- Once the PR is merged, the "baselines" are updated, and the process continues. + +This workflow solves a lot of the common problems with Visual Regression Tests. First off, **cloud machines** capture all the screenshots, so they're consistent and not dependent on different hardware. The screenshots are **not committed to the repo**, so it's easy to approve and update baselines, and avoid merge conflicts. And since this happens in CI, developers don't need to update screenshots locally. + +Here’s a quick example of the workflow in action. [This is a PR with a subtle CSS change](https://github.com/FormidableLabs/nextjs-sanity-fe/pull/204) to the color of a line. Chromatic quickly reports “1 change must be accepted”. Reviewing the change is easy, with side-by-side diffs and various ways to highlight the changes. I can Approve, Deny, or Comment on each diff. Once approved, the PR is unblocked! + + + + + +Chromatic makes it easy to see the UI changes, lets you play with them in your browser, and does a great job at ensuring your CSS is fully tested before merging. + +### Strategies for writing better tests + +One area of Storybook is still rough and could use some improvement: **test organization**. + +Most unit test frameworks use nested `describe`, `before`, `beforeEach`, and `it` blocks to organize, group, and share logic across tests. Unfortunately, Stories can only have a single `play` function. So here are a couple of strategies we use to keep things organized. + +### Use `step` to break things down + +The `play` function can get rather large; break it down using `step`! This adds structure to your tests, is self-documenting, improves test logging, and the UI even lets you execute steps 1-by-1 for debugging. Use `step` generously! + +```tsx +// ~/components/Search.stories.tsx +export const WithSearchTerm: Story = { + async play({ canvasElement, step }) { + await step("type 'baguette' into the search box", async () => { + // ... + }); + await step("expect to see a loading indicator", async () => { + // ... + }); + await step("expect to see some search results", async () => { + // ... + }); + }, +}; +``` + +### Reusable selectors + +Every `play` function interacts with elements on the page. We found it best to encapsulate the "selector logic," making it reusable across stories, and making the tests easier to understand. + +All of our stories use a `wrap` function like below, making it easy to interact with the UI: + +```tsx +// ~/components/Search.stories.tsx +function wrap(canvasElement) { + // Use `within` (from @storybook/testing-library) to target UI elements: + const container = within(canvasElement); + return { + // 👇 We name all our components, especially when the selectors are generic: + get searchbox() { return container.getByRole("searchbox"); }, + get resultsBox() { return container.getByRole("listbox"); }, + get resultItems() { return container.queryAllByRole("listitem"); }, + }; +} + +export const WithSearchTerm: Story = { + async play({ canvasElement, step }) { + const ui = wrap(canvasElement); + + await step("type 'baguette' into the search box", async () => { + // 👇 Tests are easy to read, write: + ui.searchbox.focus(); + await userEvent.type(ui.searchbox, "baguette"); + }); + + // ... + } +}; + +``` + +> Full example: [~/components/Search.stories.tsx#L113](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/storybook-blog-v1/packages/nextjs/components/Search.stories.tsx#L113) +> + +### Create "test-only" stories + +Not every test makes a good story. We often write tests that end with the UI in a messy or redundant state. Since Storybook is still a showcase of our components, we don't want to showcase these stories. + +Since there's no way to hide these Stories, we give them names that indicate they're "test-only": + +```tsx +export const WithSearchTerm: Story = { + // ... +}; +export const WithSearchTerm_Test_Cleared: Story = { + // This story clears the search, so it's a redundant Story, but a good test. + name: "With Search Term / Test / Cleared", + // ... +} +``` + +> Full example: [~/components/Search.stories.tsx#L91](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/storybook-blog-v1/packages/nextjs/components/Search.stories.tsx#L91) +> + +### Reuse the `play` functions + +Since there are no `before / beforeEach` hooks, we need a different way to reuse setup logic. +Fortunately, it's easy for a Story to call the `play` function of another story! + +For example, we have a story, `WithSearchTerm`, that enters a search term, and waits for the results to be populated. Using this as a starting point, we then want another test `WithSearchTerm_Test_Cleared` that clears the search term. We can do this by simply calling `WithSearchTerm.play` from the new story: + +```tsx +export const WithSearchTerm: Story = { + async play({ canvasElement, step }) { + // (types a search term, and waits for the results to display) + } +} + +export const WithSearchTerm_Test_Cleared: Story = { + async play({ canvasElement, step }) { + const ui = wrap(canvasElement); + + // 👇 Reuse the previous Story's steps: + await WithSearchTerm.play({ canvasElement, step }); + + await step('clear the search box', async () => { + await userEvent.clear(ui.searchbox); + }); + } +} +``` + +Full example: [~/components/Search.stories.tsx#L95](https://github.com/FormidableLabs/nextjs-sanity-fe/blob/storybook-blog-v1/packages/nextjs/components/Search.stories.tsx#L95C37-L95C37) + + +### The End Result + +After a few months using Storybook for testing, our team added tests for **over 350 components and hooks**, with **test coverage for 6000 lines of code** (over 60% of the application). We have a higher velocity now, due to this fantastic development environment. We enjoy writing stories, because they eliminate repetitive manual testing. We’ve tested parts of the application that were difficult to test otherwise. And PRs are easier to review, because screenshots are added automatically. + +The hardest part of this integration was getting components with dependencies to render in isolation, a challenge with any unit test framework. But the strategies above helped overcome this challenge, and we’re now enjoying the benefits of component-driven development and testing. diff --git a/_posts/2023-10-13-unlocking-the-power-of-storybook.md b/_posts/2023-10-13-unlocking-the-power-of-storybook.md new file mode 100644 index 0000000000000..cff8abbf06841 --- /dev/null +++ b/_posts/2023-10-13-unlocking-the-power-of-storybook.md @@ -0,0 +1,19 @@ +My latest Formidable blog post, [Unlocking the Power of Storybook](https://formidable.com/blog/2023/unlocking-the-power-of-storybook/), is now live! + +In this piece I delve into the multifaceted capabilities of Storybook, transcending its role as a UI showcase to emerge as a robust development environment and unit testing framework for frontend applications. + +Here's a sneak peek at what you'll discover: + +1️⃣ Storybook's Hidden Powers: Uncover how Storybook transforms into a development environment and unit testing framework for various aspects of your frontend app. + +2️⃣ Our Success Story: Explore how our team leverages Storybook to enhance code coverage, boost developer velocity, and streamline codebase exploration for a large e-commerce client. + +3️⃣ Managing UI Dependencies: Learn strategies to handle complex UI dependencies and seamlessly integrate Storybook into your application workflow. + +4️⃣ Automating Manual Tests: Discover the magic of Storybook Interaction Testing—turn your manual tests into automated interactions with familiar tools like Jest and Playwright. + +5️⃣ Visual Regression Testing Made Easy: Unearth the wonders of visual regression testing with Storybook and Chromatic. Ensure your CSS is rock-solid with automated screenshot tests for every story. + +6️⃣ Strategies for Better Tests: Get insights into organizing and optimizing your Storybook tests, from breaking down functions using 'step' to reusing play functions efficiently. + +Ready to elevate your frontend development game? Read the full blog and unlock the true potential of Storybook: https://formidable.com/blog/2023/unlocking-the-power-of-storybook/ diff --git a/_posts/draft-2020-8-11-typescript-is-just-tests.md b/_posts/draft-2020-8-11-typescript-is-just-tests.md new file mode 100644 index 0000000000000..a3e70808c9151 --- /dev/null +++ b/_posts/draft-2020-8-11-typescript-is-just-tests.md @@ -0,0 +1,5 @@ +--- +layout: post +title: TypeScript is just tests +--- +Draft: WIP diff --git a/about.md b/about.md index bc21f5731bf4b..e2a5174541752 100644 --- a/about.md +++ b/about.md @@ -1,15 +1,27 @@ --- layout: page -title: About +title: About Scott Rippey permalink: /about/ --- -Some information about you! -### More Information +## Hi, I'm Scott Rippey! -A place to include any other types of information that you'd like to include about yourself. +I live in Boise, Idaho, with my awesome wife and 5 kids. We like to ride bikes, play baseball, and generally just be together! We love Jesus and are raising our kids to follow Him too. -### Contact me -[email@domain.com](mailto:email@domain.com) \ No newline at end of file +## What I do + +I'm a full-stack engineer with a passion for front-end development. I love the art of writing code and seeing it come to life as a beautiful UI. +I take pride in writing code that's clean, understandable, testable, and reusable. Hopefully my Github profile showcases my passion! + + +## My Projects + +I have a couple of [interesting GitHub projects](../my-projects), so please take a look! + + +## Contact me + +You can contact me via [LinkedIn](https://www.linkedin.com/in/scottrippey/) +I'm not interested in solicitations or job offers, thank you!