diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 32ed365..0000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-tasks:
- - init: bundle install
-
-vscode:
- extensions:
- - streetsidesoftware.code-spell-checker@1.9.0:zfrDgZmxZRojZW7Uky3Fww==
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f38768a..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-sudo: required
-
-language: generic
-
-services:
- - docker
-
-script:
- - docker build -t avolpe/blog:latest .
-
-after_success:
- - if [ "$TRAVIS_BRANCH" == "master" ]; then
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD";
- docker push avolpe/blog:latest;
- curl "$HOOK_URL";
- fi
diff --git a/Dockerfile b/Dockerfile
index ce1039a..d05b9d3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,46 +1,17 @@
-# DOCKER-VERSION 1.6.0, build 4749651
+# Step 1: Build Jekyll Site
+FROM ruby:3.3.4-bullseye AS builder
-# habd.as Dockerfile
-# Runs Jekyll under Nginx with Passenger
+WORKDIR /app
-FROM phusion/passenger-ruby27:1.0.10
-MAINTAINER Arturo Volpe "arturovolpe@gmail.com"
-
-# Set environment variables
-ENV HOME /home/deployer
-
-# Set default locale for the environment
-ENV LC_ALL C.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US.UTF-8
-
-# Use baseimage-docker's init process
-CMD ["/sbin/my_init"]
-
-# Expose Nginx HTTP service
-EXPOSE 80
-
-# Start Nginx / Passenger
-RUN rm -f /etc/service/nginx/down
-
-# Remove the default site
-RUN rm /etc/nginx/sites-enabled/default
-
-# Add the Nginx site and config
-COPY nginx.conf /etc/nginx/sites-enabled/webapp.conf
-
-# Install bundle of gems
-WORKDIR /tmp
-COPY Gemfile /tmp/
-COPY Gemfile.lock /tmp/
+COPY Gemfile /app/
+COPY Gemfile.lock /app/
RUN bundle install
-#RUN gem install jekyll --no-rdoc --no-ri -v 4.1.1
-# Add the Passenger app
-COPY . /home/app/webapp
-RUN chown -R app:app /home/app/webapp
+COPY . .
-# Build the app with Jekyll
-WORKDIR /home/app/webapp
RUN bundle exec jekyll build
+# Step 2: Serve with Nginx
+FROM nginx:alpine
+
+COPY --from=builder /app/_site /usr/share/nginx/html
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index b589738..2964666 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,2 +1,2 @@
source 'https://rubygems.org'
-gem 'github-pages', '~> 206'
+gem 'github-pages', '~> 232'
diff --git a/Gemfile.lock b/Gemfile.lock
index f7b1faa..cdc0737 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,126 +1,150 @@
GEM
remote: https://rubygems.org/
specs:
- activesupport (6.0.3.2)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (>= 0.7, < 2)
- minitest (~> 5.1)
- tzinfo (~> 1.1)
- zeitwerk (~> 2.2, >= 2.2.2)
- addressable (2.8.0)
- public_suffix (>= 2.0.2, < 5.0)
+ activesupport (8.0.1)
+ base64
+ benchmark (>= 0.3)
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.3.1)
+ connection_pool (>= 2.2.5)
+ drb
+ i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
+ minitest (>= 5.1)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
+ uri (>= 0.13.1)
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ base64 (0.2.0)
+ benchmark (0.4.0)
+ bigdecimal (3.1.9)
coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.11.1)
+ coffee-script-source (1.12.2)
colorator (1.1.0)
- commonmarker (0.17.13)
- ruby-enum (~> 0.5)
- concurrent-ruby (1.1.6)
- dnsruby (1.61.3)
- addressable (~> 2.5)
- em-websocket (0.5.1)
+ commonmarker (0.23.11)
+ concurrent-ruby (1.3.5)
+ connection_pool (2.5.0)
+ csv (3.3.2)
+ dnsruby (1.72.3)
+ base64 (~> 0.2.0)
+ simpleidn (~> 0.2.1)
+ drb (2.2.1)
+ em-websocket (0.5.3)
eventmachine (>= 0.12.9)
- http_parser.rb (~> 0.6.0)
- ethon (0.12.0)
- ffi (>= 1.3.0)
+ http_parser.rb (~> 0)
+ ethon (0.16.0)
+ ffi (>= 1.15.0)
eventmachine (1.2.7)
- execjs (2.7.0)
- faraday (1.0.1)
- multipart-post (>= 1.2, < 3)
- ffi (1.13.1)
+ execjs (2.10.0)
+ faraday (2.12.2)
+ faraday-net_http (>= 2.0, < 3.5)
+ json
+ logger
+ faraday-net_http (3.4.0)
+ net-http (>= 0.5.0)
+ ffi (1.17.1)
forwardable-extended (2.6.0)
- gemoji (3.0.1)
- github-pages (206)
- github-pages-health-check (= 1.16.1)
- jekyll (= 3.8.7)
- jekyll-avatar (= 0.7.0)
- jekyll-coffeescript (= 1.1.1)
- jekyll-commonmark-ghpages (= 0.1.6)
- jekyll-default-layout (= 0.1.4)
- jekyll-feed (= 0.13.0)
+ gemoji (4.1.0)
+ github-pages (232)
+ github-pages-health-check (= 1.18.2)
+ jekyll (= 3.10.0)
+ jekyll-avatar (= 0.8.0)
+ jekyll-coffeescript (= 1.2.2)
+ jekyll-commonmark-ghpages (= 0.5.1)
+ jekyll-default-layout (= 0.1.5)
+ jekyll-feed (= 0.17.0)
jekyll-gist (= 1.5.0)
- jekyll-github-metadata (= 2.13.0)
- jekyll-mentions (= 1.5.1)
+ jekyll-github-metadata (= 2.16.1)
+ jekyll-include-cache (= 0.2.1)
+ jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
- jekyll-redirect-from (= 0.15.0)
+ jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1)
- jekyll-remote-theme (= 0.4.1)
+ jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2)
- jekyll-seo-tag (= 2.6.1)
+ jekyll-seo-tag (= 2.8.0)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
- jekyll-theme-architect (= 0.1.1)
- jekyll-theme-cayman (= 0.1.1)
- jekyll-theme-dinky (= 0.1.1)
- jekyll-theme-hacker (= 0.1.1)
- jekyll-theme-leap-day (= 0.1.1)
- jekyll-theme-merlot (= 0.1.1)
- jekyll-theme-midnight (= 0.1.1)
- jekyll-theme-minimal (= 0.1.1)
- jekyll-theme-modernist (= 0.1.1)
- jekyll-theme-primer (= 0.5.4)
- jekyll-theme-slate (= 0.1.1)
- jekyll-theme-tactile (= 0.1.1)
- jekyll-theme-time-machine (= 0.1.1)
+ jekyll-theme-architect (= 0.2.0)
+ jekyll-theme-cayman (= 0.2.0)
+ jekyll-theme-dinky (= 0.2.0)
+ jekyll-theme-hacker (= 0.2.0)
+ jekyll-theme-leap-day (= 0.2.0)
+ jekyll-theme-merlot (= 0.2.0)
+ jekyll-theme-midnight (= 0.2.0)
+ jekyll-theme-minimal (= 0.2.0)
+ jekyll-theme-modernist (= 0.2.0)
+ jekyll-theme-primer (= 0.6.0)
+ jekyll-theme-slate (= 0.2.0)
+ jekyll-theme-tactile (= 0.2.0)
+ jekyll-theme-time-machine (= 0.2.0)
jekyll-titles-from-headings (= 0.5.3)
- jemoji (= 0.11.1)
- kramdown (= 1.17.0)
- liquid (= 4.0.3)
+ jemoji (= 0.13.0)
+ kramdown (= 2.4.0)
+ kramdown-parser-gfm (= 1.1.0)
+ liquid (= 4.0.4)
mercenary (~> 0.3)
minima (= 2.5.1)
- nokogiri (>= 1.10.4, < 2.0)
- rouge (= 3.19.0)
+ nokogiri (>= 1.16.2, < 2.0)
+ rouge (= 3.30.0)
terminal-table (~> 1.4)
- github-pages-health-check (1.16.1)
+ webrick (~> 1.8)
+ github-pages-health-check (1.18.2)
addressable (~> 2.3)
dnsruby (~> 1.60)
- octokit (~> 4.0)
- public_suffix (~> 3.0)
+ octokit (>= 4, < 8)
+ public_suffix (>= 3.0, < 6.0)
typhoeus (~> 1.3)
- html-pipeline (2.13.0)
+ html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
- http_parser.rb (0.6.0)
- i18n (0.9.5)
+ http_parser.rb (0.8.0)
+ i18n (1.14.7)
concurrent-ruby (~> 1.0)
- jekyll (3.8.7)
+ jekyll (3.10.0)
addressable (~> 2.4)
colorator (~> 1.0)
+ csv (~> 3.0)
em-websocket (~> 0.5)
- i18n (~> 0.7)
+ i18n (>= 0.7, < 2)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
- kramdown (~> 1.14)
+ kramdown (>= 1.17, < 3)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
- jekyll-avatar (0.7.0)
+ webrick (>= 1.0)
+ jekyll-avatar (0.8.0)
jekyll (>= 3.0, < 5.0)
- jekyll-coffeescript (1.1.1)
+ jekyll-coffeescript (1.2.2)
coffee-script (~> 2.2)
- coffee-script-source (~> 1.11.1)
- jekyll-commonmark (1.3.1)
- commonmarker (~> 0.14)
- jekyll (>= 3.7, < 5.0)
- jekyll-commonmark-ghpages (0.1.6)
- commonmarker (~> 0.17.6)
- jekyll-commonmark (~> 1.2)
- rouge (>= 2.0, < 4.0)
- jekyll-default-layout (0.1.4)
- jekyll (~> 3.0)
- jekyll-feed (0.13.0)
+ coffee-script-source (~> 1.12)
+ jekyll-commonmark (1.4.0)
+ commonmarker (~> 0.22)
+ jekyll-commonmark-ghpages (0.5.1)
+ commonmarker (>= 0.23.7, < 1.1.0)
+ jekyll (>= 3.9, < 4.0)
+ jekyll-commonmark (~> 1.4.0)
+ rouge (>= 2.0, < 5.0)
+ jekyll-default-layout (0.1.5)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
- jekyll-github-metadata (2.13.0)
+ jekyll-github-metadata (2.16.1)
jekyll (>= 3.4, < 5.0)
- octokit (~> 4.0, != 4.4.0)
- jekyll-mentions (1.5.1)
+ octokit (>= 4, < 7, != 4.4.0)
+ jekyll-include-cache (0.2.1)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-mentions (1.6.0)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
@@ -128,121 +152,131 @@ GEM
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
- jekyll-redirect-from (0.15.0)
+ jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
- jekyll-remote-theme (0.4.1)
+ jekyll-remote-theme (0.4.3)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
- rubyzip (>= 1.3.0)
+ jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
+ rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
- jekyll-seo-tag (2.6.1)
- jekyll (>= 3.3, < 5.0)
+ jekyll-seo-tag (2.8.0)
+ jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
- jekyll-theme-architect (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-architect (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-cayman (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-cayman (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-dinky (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-dinky (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-hacker (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-hacker (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-leap-day (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-leap-day (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-merlot (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-merlot (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-midnight (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-midnight (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-minimal (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-minimal (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-modernist (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-modernist (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-primer (0.5.4)
+ jekyll-theme-primer (0.6.0)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-slate (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-slate (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-tactile (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-tactile (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
- jekyll-theme-time-machine (0.1.1)
- jekyll (~> 3.5)
+ jekyll-theme-time-machine (0.2.0)
+ jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
- jemoji (0.11.1)
- gemoji (~> 3.0)
+ jemoji (0.13.0)
+ gemoji (>= 3, < 5)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
- kramdown (1.17.0)
- liquid (4.0.3)
- listen (3.2.1)
+ json (2.9.1)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (4.0.4)
+ listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
+ logger (1.6.5)
mercenary (0.3.6)
- mini_portile2 (2.4.0)
+ mini_portile2 (2.8.8)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
- minitest (5.14.1)
- multipart-post (2.1.1)
- nokogiri (1.10.10)
- mini_portile2 (~> 2.4.0)
- octokit (4.18.0)
- faraday (>= 0.9)
- sawyer (~> 0.8.0, >= 0.5.3)
+ minitest (5.25.4)
+ net-http (0.6.0)
+ uri
+ nokogiri (1.18.2)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ octokit (4.25.1)
+ faraday (>= 1, < 3)
+ sawyer (~> 0.9)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
- public_suffix (3.1.1)
- rb-fsevent (0.10.4)
- rb-inotify (0.10.1)
+ public_suffix (5.1.1)
+ racc (1.8.1)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.11.1)
ffi (~> 1.0)
- rouge (3.19.0)
- ruby-enum (0.8.0)
- i18n
- rubyzip (2.3.0)
+ rexml (3.4.0)
+ rouge (3.30.0)
+ rubyzip (2.4.1)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
- sawyer (0.8.2)
+ sawyer (0.9.2)
addressable (>= 2.3.5)
- faraday (> 0.8, < 2.0)
+ faraday (>= 0.17.3, < 3)
+ securerandom (0.4.1)
+ simpleidn (0.2.3)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
- thread_safe (0.3.6)
- typhoeus (1.4.0)
+ typhoeus (1.4.1)
ethon (>= 0.9.0)
- tzinfo (1.2.7)
- thread_safe (~> 0.1)
- unicode-display_width (1.7.0)
- zeitwerk (2.4.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unicode-display_width (1.8.0)
+ uri (1.0.2)
+ webrick (1.9.1)
PLATFORMS
ruby
DEPENDENCIES
- github-pages (~> 206)
+ github-pages (~> 232)
BUNDLED WITH
2.1.1
diff --git a/README.md b/README.md
index ab022b2..1da0e94 100644
--- a/README.md
+++ b/README.md
@@ -8,3 +8,8 @@
> This project forked and has been modified from [My Stack Problems](https://github.com/agusmakmun/agusmakmun.github.io),
> and the search posts using [Super Search](https://github.com/chinchang/super-search)
+
+
+### Notas
+
+* Copy themes from https://numist.github.io/highlight-css/ and put it in ./static/css/syntax.css
\ No newline at end of file
diff --git a/_config.yml b/_config.yml
index 9f37675..d02ab38 100644
--- a/_config.yml
+++ b/_config.yml
@@ -60,3 +60,21 @@ compress_html:
clippings: all
comments: [""]
endings: all
+
+exclude:
+ - .sass-cache/
+ - .jekyll-cache/
+ - gemfiles/
+ - Gemfile
+ - Gemfile.lock
+ - node_modules/
+ - vendor/bundle/
+ - vendor/cache/
+ - vendor/gems/
+ - vendor/ruby/
+ - docker-compose.yml
+ - Dockerfile
+ - run.sh
+ - LICENSE
+ - README.md
+ - nginx.conf
\ No newline at end of file
diff --git a/_layouts/default.html b/_layouts/default.html
index dcb3a5d..eaa5dc5 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -125,9 +125,16 @@
-
-
-
+
+
+
diff --git a/_layouts/post.html b/_layouts/post.html
index 3f59b32..22cfb49 100644
--- a/_layouts/post.html
+++ b/_layouts/post.html
@@ -14,7 +14,6 @@
{{ page.title }}
{{ content }}
- {% include share-page.html %}
{% assign hasSimilar = '' %}
@@ -53,21 +52,3 @@
Related Posts
{{ page.next.title }} »
{% endif %}
-
-
diff --git a/_posts/2025-01-31-tester-pattern-react.md b/_posts/2025-01-31-tester-pattern-react.md
new file mode 100644
index 0000000..90f89b6
--- /dev/null
+++ b/_posts/2025-01-31-tester-pattern-react.md
@@ -0,0 +1,407 @@
+---
+layout: post
+title: "[WIP] The tester pattern in react"
+description: "Explorint the testern pattern in a react project"
+category: "develop"
+tags: ["javascript", "typescript", "react", "vite", "react-testing-library"]
+---
+
+The [tester pattern](https://www.testerpattern.nl/pattern) is a good way to structure your tests to improve the readability and to make very easy to write and expand the test suite.
+
+In the original webpage, the author explain the concepts in java, in this blog we will try the pattern in JavaScript, more specifically in a react SPA.
+
+The libraries that we will be using are:
+
+- vitest as the runner and mock library
+- react testing library to interact with react components.
+
+Let's explore the pattern in various examples with an increase level of
+complexity:
+
+
+- a simple component that draws a static message
+- a form with various inputs.
+- the same component, but the message is a quote from a web service. Using MSW for the test.
+
+
+# first scenario: simple component
+
+Let's test this component:
+
+```tsx
+export const HelloWorld = (props) => (
+
+ Hello {props.name}
+
+)
+```
+
+To test the rendering of this component, we can this test:
+
+```tsx
+describe('simpleComponent', () => {
+ it('renderAndUpdateGreeting', async () => {
+ const tester = new HelloWorldPageTester()
+ .withName('Arturo');
+
+ let asserter = await tester.whenRender();
+ await asserter.hasText("Hello Arturo");
+
+ tester.withName('Volpe');
+ asserter = await tester.whenRerender();
+ await asserter.hasText("Hello Volpe");
+ });
+});
+```
+
+For this simple case the boilerplate of adding a tester and asserter helper
+classes may be too much, but an interesting side-effect is that the test doesn't
+have any component-related selector.
+
+The test is in 'plain' english (with the limitation of the language) and the helper classes can be user for multiple tests.
+
+The tester and asserted used:
+
+```tsx
+class HelloWorldPageTester {
+
+ name: String = "";
+ component!: RenderResult;
+
+ withName(name: string) { this.name = name; return this; }
+
+ whenRender() {
+ this.component = render(
);
+ return new HelloWorldPageAsserter(this.component);
+ }
+
+ whenRerender() {
+ this.component.rerender(
);
+ return new HelloWorldPageAsserter(this.component);
+ }
+}
+
+class HelloWorldPageAsserter {
+ constructor(private component: RenderResult) {}
+
+ async hasText(expectedText: string) {
+ await this.component.findByText(expectedText);
+ return this;
+ }
+}
+```
+
+Links:
+
+* Component: [HelloWorld.tsx](https://github.com/aVolpe/vitest-tester-pattern-playground/blob/main/src/HelloWorld.tsx)
+* Test: [HelloWorld.test.tsx](https://github.com/aVolpe/vitest-tester-pattern-playground/blob/main/src/HelloWorld.test.tsx)
+
+## A note about async/await
+
+Ideally we want a fluent syntax when using the tester pattern, for example in
+Java we can have a bunch of asserts chained:
+
+```java
+new BookTester()
+ .givenAuthor("Arturo Volpe")
+ .givenHasChapter("1 - VITE")
+ .givenHasChapter("2 - The pattern")
+ .whenPublish() // return asserter
+ .thenHasInCover("Author: Arturo Volpe")
+ .thenHasInChapters(2);
+```
+
+To map this to a `react-testing-library` test, we need to use async/await,
+normally the asserter methods are required to be async in order to use the
+[findBy
+methods](https://testing-library.com/docs/dom-testing-library/api-async#findby-queries),
+so we need to migrate to:
+
+```typescript
+const tester = new BookComponentTester()
+ .givenAuthor("Arturo Volpe")
+ .givenHasChapter("1 - VITE")
+ .givenHasChapter("2 - The pattern");
+
+// normally here we render/fill the 'page'
+const asserter = await tester.whenRender();
+
+await asserter.thenHasInCover("Author: Arturo Volpe");
+await asserter.thenHasInChapters(2);
+```
+
+Maybe if someday the [pipeline
+operator](https://github.com/tc39/proposal-pipeline-operator?tab=readme-ov-file)
+added to the language, this will be more concise:
+
+```typescript
+new BookComponentTester()
+ .givenAuthor("Arturo Volpe")
+ .givenHasChapter("1 - VITE")
+ .givenHasChapter("2 - The pattern")
+ // normally here we render/fill the 'page'
+ |> await %.whenRender()
+ |> await %.thenHasInCover("Author: Arturo Volpe")
+ |> await %.thenHasInChapters(2)
+```
+
+# Second scenario: A simple form
+
+The tester pattern realy shines when we need to test different use cases that
+has a similar setup and need specific assertions, in this case we will have a
+form to edit some personal information (name and lastname).
+
+For the form we will use
+[react-hook-form](https://react-hook-form.com/get-started), and the [getting
+started page](https://react-hook-form.com/get-started) form with some small
+modifications.
+
+In this form, we have two inputs:
+
+* name: the first name, required, max length 10
+* lastname: the last name, not required, max length 20
+
+```tsx
+type Inputs = {
+ lastname: string
+ firstname: string
+}
+
+export function Step2Form(props: {
+ onSubmit: (dat: Inputs) => void
+}) {
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors },
+ } = useForm
()
+ const onSubmit: SubmitHandler = (data) => props.onSubmit(data);
+
+ return
+
+}
+```
+
+> Some spans are added to make the test easier to write, in the real world,
+those messages may appear under the inputs or in other parts of the form, the
+`data-testid` may be replaced with `findByRole`, etc.
+
+Ideally we want very descriptive tests for this form, an easy way to write all
+the different cases, required fields, invalid lengths, etc, lets start with a
+simple test that check that the 'happy path' works:
+
+```tsx
+describe('step2Form', () => {
+ it('renderValidValues', async () => {
+ const tester = new Step2FormPageTester()
+ .withName('Arturo')
+ .withLastname('Volpe');
+
+ let asserter = await tester.whenDoSubmit();
+ await asserter.hasAllFieldsValid();
+ });
+});
+```
+
+In this test we create a tester that populates both fields and clicks the
+button, in this case the tester is like a robot, we give the tester some
+instructions, and the tester execute the required steps.
+
+Reading this test we don't know anything about the internals of the component,
+we only know that we are asking the tester to submit the form with a given name
+and lastname. **The internals of the component and the complexity of the
+emulation of user actions is hidden to the test, making it easy to read and
+follow**.
+
+The tester for this form is slightly more complex:
+
+```tsx
+class Step2FormPageTester {
+
+ name: String = "";
+ lastname: String = "";
+ component!: RenderResult;
+
+ withName(name: string) { this.name = name; return this; }
+ withLastname(lastname: string) { this.lastname = lastname; return this; }
+
+ async whenDoSubmit() {
+ this.component = render();
+
+ await this.fillInput("firstname-input", this.name);
+ await this.fillInput("lastname-input", this.lastname);
+
+ return new Step2FormPageAsserter(this.component, this.submitCallback);
+ }
+
+ async fillInput(targetTestId: string, toInsert: string) {
+ const input = await this.component.findByTestId(targetTestId);
+ fireEvent.change(input, {target: {value: toInsert}})
+ return this;
+ }
+
+}
+
+class Step2FormPageAsserter {
+ constructor(private component: RenderResult,
+ private callback: (dat: Inputs) => void) {}
+
+ async hasText(expectedText: string) {
+ await this.component.findByText(expectedText);
+ return this;
+ }
+
+ async hasAllFieldsValid() {
+ return this.hasText('The form has 0 errors');
+ }
+}
+```
+
+We can see that there are some 'component kwnoledge' in the Tester, the tester
+knows how to find the desired inputs, and that logic, specially when we are
+testing front end components is very tricky, sometimes we need to wait it to
+render, sometimes we need to use a complex css selector, but all of that is
+hidden and is a implementation detail of the Tester.
+
+## Adding more tests cases
+
+Once we write the tester, adding more scenarios is trivial:
+
+```tsx
+ it('validateEmptyFields', async () => {
+ const tester = new Step2FormPageTester()
+ .withName('')
+ .withLastname('');
+
+ let asserter = await tester.whenDoSubmit();
+ await asserter.hasInvalidFieldsCount(1); // only the name is required
+ await asserter.hasInvalidFirstName();
+ });
+
+ it('validateInvalidFields', async () => {
+ const tester = new Step2FormPageTester()
+ .withName('super large name that exceed the expected length')
+ .withLastname('super large lastname that exceed the expected length');
+
+ let asserter = await tester.whenDoSubmit();
+ await asserter.hasInvalidFieldsCount(2);
+ await asserter.hasInvalidFirstName();
+ });
+```
+
+Adding two new tests only required two new assertions:
+
+```tsx
+// FormAsserter
+ async hasInvalidFieldsCount(expectedCount: number) {
+ return this.hasText(`The form has ${expectedCount} errors`);
+ }
+
+ async hasInvalidFirstName() {
+ return this.hasText(`This field is invalid`);
+ }
+```
+
+Using the tester pattern, adding more tests is easy once we have the tester and
+the asserter with many features.
+
+## An extra complexity, verifying the invocation to the callback
+
+The `Step2Form` component receives a callback as a property, this callback is
+invoked only when the form is valid, the first argument of the function is the
+valid object.
+
+To verify that the function is only called when we have a valid form, we need
+to mock a call, lets do that in the Tester:
+
+```tsx
+class Step2FormPageTester {
+
+ // ...
+ submitCallback: (dat: Inputs) => void = vi.fn();
+ // ...
+
+ async whenDoSubmit() {
+ // .. same as before
+
+ const bttn = await this.component
+ .findByText('Save');
+
+ fireEvent.click(bttn);
+
+ return new Step2FormPageAsserter(this.component, this.submitCallback);
+ }
+```
+
+> Here we can have two differente asserts, one to check for contents in the
+page, and another one to assert the invocations to the function. For simplicity,
+we will only use one.
+
+With this mock (`vn.fn()`) function, we can add assertions:
+
+```tsx
+class Step2FormPageAsserter {
+ constructor(private component: RenderResult,
+ private callback: (dat: Inputs) => void) {}
+
+ // ...
+
+ async callbackWasNotCalled() {
+ expect(this.callback).toHaveBeenCalledTimes(0);
+ return this;
+ }
+
+ async callbackWasCalledWith(expected: Inputs) {
+ expect(this.callback).toHaveBeenCalledWith(expected);
+ return this;
+ }
+}
+```
+
+And modify the tests:
+
+```tsx
+ it('renderValidValues', async () => {
+ const tester = new Step2FormPageTester()
+ .withName('Arturo')
+ .withLastname('Volpe');
+
+ let asserter = await tester.whenDoSubmit();
+ await asserter.hasAllFieldsValid();
+ await asserter.callbackWasCalledWith({
+ firstname: 'Arturo',
+ lastname: 'Volpe'
+ });
+ });
+ it('validateEmptyFields', async () => {
+ const tester = new Step2FormPageTester()
+ .withName('')
+ .withLastname('');
+
+ let asserter = await tester.whenDoSubmit();
+ await asserter.hasInvalidFieldsCount(1);
+ await asserter.hasInvalidFirstName();
+ await asserter.callbackWasNotCalled();
+ });
+```
+
+
+Links:
+
+* Component: [Step2Form.tsx](https://github.com/aVolpe/vitest-tester-pattern-playground/blob/main/src/Step2Form.tsx)
+* Test: [Step2Form.test.tsx](https://github.com/aVolpe/vitest-tester-pattern-playground/blob/main/src/Step2Form.test.tsx)
+
+All the source code for the examples is in the [vitest pattern playground github repo](https://github.com/aVolpe/vitest-tester-pattern-playground/tree/main)
\ No newline at end of file
diff --git a/static/css/syntax.css b/static/css/syntax.css
index 62eedce..6424046 100644
--- a/static/css/syntax.css
+++ b/static/css/syntax.css
@@ -1,59 +1,86 @@
-.highlight .hll { background-color: #49483e }
-.highlight .c { color: #75715e } /* Comment */
-.highlight .err { color: #960050; background-color: #1e0010 } /* Error */
-.highlight .k { color: #66d9ef } /* Keyword */
-.highlight .l { color: #ae81ff } /* Literal */
-.highlight .n { color: #f8f8f2 } /* Name */
-.highlight .o { color: #f92672 } /* Operator */
-.highlight .p { color: #f8f8f2 } /* Punctuation */
-.highlight .cm { color: #75715e } /* Comment.Multiline */
-.highlight .cp { color: #75715e } /* Comment.Preproc */
-.highlight .c1 { color: #75715e } /* Comment.Single */
-.highlight .cs { color: #75715e } /* Comment.Special */
-.highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gs { font-weight: bold } /* Generic.Strong */
-.highlight .kc { color: #66d9ef } /* Keyword.Constant */
-.highlight .kd { color: #66d9ef } /* Keyword.Declaration */
-.highlight .kn { color: #f92672 } /* Keyword.Namespace */
-.highlight .kp { color: #66d9ef } /* Keyword.Pseudo */
-.highlight .kr { color: #66d9ef } /* Keyword.Reserved */
-.highlight .kt { color: #66d9ef } /* Keyword.Type */
-.highlight .ld { color: #e6db74 } /* Literal.Date */
-.highlight .m { color: #ae81ff } /* Literal.Number */
-.highlight .s { color: #e6db74 } /* Literal.String */
-.highlight .na { color: #a6e22e } /* Name.Attribute */
-.highlight .nb { color: #f8f8f2 } /* Name.Builtin */
-.highlight .nc { color: #a6e22e } /* Name.Class */
-.highlight .no { color: #66d9ef } /* Name.Constant */
-.highlight .nd { color: #a6e22e } /* Name.Decorator */
-.highlight .ni { color: #f8f8f2 } /* Name.Entity */
-.highlight .ne { color: #a6e22e } /* Name.Exception */
-.highlight .nf { color: #a6e22e } /* Name.Function */
-.highlight .nl { color: #f8f8f2 } /* Name.Label */
-.highlight .nn { color: #f8f8f2 } /* Name.Namespace */
-.highlight .nx { color: #a6e22e } /* Name.Other */
-.highlight .py { color: #f8f8f2 } /* Name.Property */
-.highlight .nt { color: #f92672 } /* Name.Tag */
-.highlight .nv { color: #f8f8f2 } /* Name.Variable */
-.highlight .ow { color: #f92672 } /* Operator.Word */
-.highlight .w { color: #f8f8f2 } /* Text.Whitespace */
-.highlight .mf { color: #ae81ff } /* Literal.Number.Float */
-.highlight .mh { color: #ae81ff } /* Literal.Number.Hex */
-.highlight .mi { color: #ae81ff } /* Literal.Number.Integer */
-.highlight .mo { color: #ae81ff } /* Literal.Number.Oct */
-.highlight .sb { color: #e6db74 } /* Literal.String.Backtick */
-.highlight .sc { color: #e6db74 } /* Literal.String.Char */
-.highlight .sd { color: #e6db74 } /* Literal.String.Doc */
-.highlight .s2 { color: #e6db74 } /* Literal.String.Double */
-.highlight .se { color: #ae81ff } /* Literal.String.Escape */
-.highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */
-.highlight .si { color: #e6db74 } /* Literal.String.Interpol */
-.highlight .sx { color: #e6db74 } /* Literal.String.Other */
-.highlight .sr { color: #e6db74 } /* Literal.String.Regex */
-.highlight .s1 { color: #e6db74 } /* Literal.String.Single */
-.highlight .ss { color: #e6db74 } /* Literal.String.Symbol */
-.highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
-.highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */
-.highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */
-.highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */
-.highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */
+/* This file was generated using `pygmentize -S nord-darker -f html -a .highlight` */
+pre { line-height: 125%; }
+td.linenos .normal { color: #D8DEE9; background-color: #242933; padding-left: 5px; padding-right: 5px; }
+span.linenos { color: #D8DEE9; background-color: #242933; padding-left: 5px; padding-right: 5px; }
+td.linenos .special { color: #242933; background-color: #D8DEE9; padding-left: 5px; padding-right: 5px; }
+span.linenos.special { color: #242933; background-color: #D8DEE9; padding-left: 5px; padding-right: 5px; }
+.highlight .hll { background-color: #3B4252 }
+.highlight { background: #242933; color: #d8dee9 }
+.highlight .c { color: #616e87; font-style: italic } /* Comment */
+.highlight .err { color: #bf616a } /* Error */
+.highlight .esc { color: #d8dee9 } /* Escape */
+.highlight .g { color: #d8dee9 } /* Generic */
+.highlight .k { color: #81a1c1; font-weight: bold } /* Keyword */
+.highlight .l { color: #d8dee9 } /* Literal */
+.highlight .n { color: #d8dee9 } /* Name */
+.highlight .o { color: #81a1c1; font-weight: bold } /* Operator */
+.highlight .x { color: #d8dee9 } /* Other */
+.highlight .p { color: #eceff4 } /* Punctuation */
+.highlight .ch { color: #616e87; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #616e87; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #5e81ac; font-style: italic } /* Comment.Preproc */
+.highlight .cpf { color: #616e87; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #616e87; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #616e87; font-style: italic } /* Comment.Special */
+.highlight .gd { color: #bf616a } /* Generic.Deleted */
+.highlight .ge { color: #d8dee9; font-style: italic } /* Generic.Emph */
+.highlight .ges { color: #d8dee9 } /* Generic.EmphStrong */
+.highlight .gr { color: #bf616a } /* Generic.Error */
+.highlight .gh { color: #88c0d0; font-weight: bold } /* Generic.Heading */
+.highlight .gi { color: #a3be8c } /* Generic.Inserted */
+.highlight .go { color: #d8dee9 } /* Generic.Output */
+.highlight .gp { color: #616e88; font-weight: bold } /* Generic.Prompt */
+.highlight .gs { color: #d8dee9; font-weight: bold } /* Generic.Strong */
+.highlight .gu { color: #88c0d0; font-weight: bold } /* Generic.Subheading */
+.highlight .gt { color: #bf616a } /* Generic.Traceback */
+.highlight .kc { color: #81a1c1; font-weight: bold } /* Keyword.Constant */
+.highlight .kd { color: #81a1c1; font-weight: bold } /* Keyword.Declaration */
+.highlight .kn { color: #81a1c1; font-weight: bold } /* Keyword.Namespace */
+.highlight .kp { color: #81a1c1 } /* Keyword.Pseudo */
+.highlight .kr { color: #81a1c1; font-weight: bold } /* Keyword.Reserved */
+.highlight .kt { color: #81a1c1 } /* Keyword.Type */
+.highlight .ld { color: #d8dee9 } /* Literal.Date */
+.highlight .m { color: #b48ead } /* Literal.Number */
+.highlight .s { color: #a3be8c } /* Literal.String */
+.highlight .na { color: #8fbcbb } /* Name.Attribute */
+.highlight .nb { color: #81a1c1 } /* Name.Builtin */
+.highlight .nc { color: #8fbcbb } /* Name.Class */
+.highlight .no { color: #8fbcbb } /* Name.Constant */
+.highlight .nd { color: #d08770 } /* Name.Decorator */
+.highlight .ni { color: #d08770 } /* Name.Entity */
+.highlight .ne { color: #bf616a } /* Name.Exception */
+.highlight .nf { color: #88c0d0 } /* Name.Function */
+.highlight .nl { color: #d8dee9 } /* Name.Label */
+.highlight .nn { color: #8fbcbb } /* Name.Namespace */
+.highlight .nx { color: #d8dee9 } /* Name.Other */
+.highlight .py { color: #d8dee9 } /* Name.Property */
+.highlight .nt { color: #81a1c1 } /* Name.Tag */
+.highlight .nv { color: #d8dee9 } /* Name.Variable */
+.highlight .ow { color: #81a1c1; font-weight: bold } /* Operator.Word */
+.highlight .pm { color: #eceff4 } /* Punctuation.Marker */
+.highlight .w { color: #d8dee9 } /* Text.Whitespace */
+.highlight .mb { color: #b48ead } /* Literal.Number.Bin */
+.highlight .mf { color: #b48ead } /* Literal.Number.Float */
+.highlight .mh { color: #b48ead } /* Literal.Number.Hex */
+.highlight .mi { color: #b48ead } /* Literal.Number.Integer */
+.highlight .mo { color: #b48ead } /* Literal.Number.Oct */
+.highlight .sa { color: #a3be8c } /* Literal.String.Affix */
+.highlight .sb { color: #a3be8c } /* Literal.String.Backtick */
+.highlight .sc { color: #a3be8c } /* Literal.String.Char */
+.highlight .dl { color: #a3be8c } /* Literal.String.Delimiter */
+.highlight .sd { color: #616e87 } /* Literal.String.Doc */
+.highlight .s2 { color: #a3be8c } /* Literal.String.Double */
+.highlight .se { color: #ebcb8b } /* Literal.String.Escape */
+.highlight .sh { color: #a3be8c } /* Literal.String.Heredoc */
+.highlight .si { color: #a3be8c } /* Literal.String.Interpol */
+.highlight .sx { color: #a3be8c } /* Literal.String.Other */
+.highlight .sr { color: #ebcb8b } /* Literal.String.Regex */
+.highlight .s1 { color: #a3be8c } /* Literal.String.Single */
+.highlight .ss { color: #a3be8c } /* Literal.String.Symbol */
+.highlight .bp { color: #81a1c1 } /* Name.Builtin.Pseudo */
+.highlight .fm { color: #88c0d0 } /* Name.Function.Magic */
+.highlight .vc { color: #d8dee9 } /* Name.Variable.Class */
+.highlight .vg { color: #d8dee9 } /* Name.Variable.Global */
+.highlight .vi { color: #d8dee9 } /* Name.Variable.Instance */
+.highlight .vm { color: #d8dee9 } /* Name.Variable.Magic */
+.highlight .il { color: #b48ead } /* Literal.Number.Integer.Long */
\ No newline at end of file