From 66cce0fd37aa3dff936e18f90558fe334dc52038 Mon Sep 17 00:00:00 2001 From: Arturo Volpe Date: Sat, 8 Feb 2025 00:22:13 +0000 Subject: [PATCH 1/5] feat: Update github-pages gem --- Gemfile | 2 +- Gemfile.lock | 310 ++++++++++++++++++++++++++++----------------------- 2 files changed, 173 insertions(+), 139 deletions(-) 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 From eea19019244d6489c80ff4c2cc797cf1cfad5600 Mon Sep 17 00:00:00 2001 From: Arturo Volpe Date: Sat, 8 Feb 2025 02:52:43 +0000 Subject: [PATCH 2/5] feat: Improve syntax highligth and comments and share Comments and share are rarely used features of this blog. --- .gitpod.yml | 6 -- .travis.yml | 16 ----- README.md | 5 ++ _config.yml | 2 +- _layouts/post.html | 19 ------ static/css/syntax.css | 145 +++++++++++++++++++++++++----------------- 6 files changed, 92 insertions(+), 101 deletions(-) delete mode 100644 .gitpod.yml delete mode 100644 .travis.yml 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/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..cf602d2 100644 --- a/_config.yml +++ b/_config.yml @@ -59,4 +59,4 @@ plugins: compress_html: clippings: all comments: [""] - endings: all + endings: all \ No newline at end of file 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

{% endif %} - -
-
- -
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 From 0f5117d1d422bfd0d4c88f3eff93bb6d5d8cd163 Mon Sep 17 00:00:00 2001 From: Arturo Volpe Date: Sat, 8 Feb 2025 02:53:03 +0000 Subject: [PATCH 3/5] feat: Add blog about the tester pattern Is a WIP. --- _posts/2025-01-31-tester-pattern-react.md | 395 ++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 _posts/2025-01-31-tester-pattern-react.md 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..98be17f --- /dev/null +++ b/_posts/2025-01-31-tester-pattern-react.md @@ -0,0 +1,395 @@ +--- +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; + } +} +``` + + +## 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
+ + + {errors.firstname + ? This field is invalid + : The firstname is valid + } + + + + The form has {Object.keys(errors).length} errors + +
+ +} +``` + +> 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(); + }); +``` From a5cd45ec19af77af441f87c2284db95c0e2cc1e6 Mon Sep 17 00:00:00 2001 From: Arturo Volpe Date: Sat, 8 Feb 2025 03:09:27 +0000 Subject: [PATCH 4/5] fix: Ribbon fork me on github --- _layouts/default.html | 13 ++++++++++--- _posts/2025-01-31-tester-pattern-react.md | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) 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 @@ - - Fork me on GitHub - + + Fork me on GitHub +
diff --git a/_posts/2025-01-31-tester-pattern-react.md b/_posts/2025-01-31-tester-pattern-react.md index 98be17f..90f89b6 100644 --- a/_posts/2025-01-31-tester-pattern-react.md +++ b/_posts/2025-01-31-tester-pattern-react.md @@ -91,6 +91,10 @@ class HelloWorldPageAsserter { } ``` +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 @@ -393,3 +397,11 @@ And modify the tests: 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 From 99ddd2243d82475d2d37a7f5469211302f5b5f84 Mon Sep 17 00:00:00 2001 From: Arturo Volpe Date: Sat, 8 Feb 2025 03:37:07 +0000 Subject: [PATCH 5/5] feat: Simplify docker build Use a builder to build the blog, and then a nginx to serve the static blog. --- Dockerfile | 49 ++++++++++--------------------------------------- _config.yml | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 40 deletions(-) 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/_config.yml b/_config.yml index cf602d2..d02ab38 100644 --- a/_config.yml +++ b/_config.yml @@ -59,4 +59,22 @@ plugins: compress_html: clippings: all comments: [""] - endings: all \ No newline at end of file + 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