🚀

Astro + playwright + huskyでポートフォリオサイトを作ってみる

2023/01/20に公開

最近State of JS2022が発表されて、Astroがいい感じに順位上げて注目されてきたので、Astro 製のポートフォリオサイトを構築してみました。

今までは React でアウトプットの練習がてらポートフォリオサイトを作っていましたが、それを Astro に移行する形で作りました。

デザインに関してMUIを使っていたのですが、Astro ではCSS-in-JSのサポートがまだ不完全なよう(参考)なので、daisyUIに移行しました。(Tailwind CSS初めて使いましたが、めちゃ便利ですね。。。。)

作成したサイトは下記にあるので、誰かの参考になれば幸いです。

https://okojomoeko.github.io/

移行前と移行後で何が変わったのか

変わったところ 移行前 移行後
フロントエンド React Astro
ビルドツール Vite Vite(Astro では Vite を使っている)
UI フレームワーク MUI daisyUI, Tailwind CSS
テストフレームワーク Jest PlayWright
コンテンツをどう準備するか Json みたいな設定ファイル MDX
サイトの構成 SPA MPA

と、こんな感じになりました。

どうやってポートフォリオ作っていったか

イチから Astro を勉強してやっていっても良かったのですが、習うより慣れろという形で、まずは先人たちのテンプレを参考にさせていただいて、自分なりに改造していく感じで実装していきました。

Astro テンプレートを選ぶ

Astro 初心者でも簡単にプロジェクトを作れるように様々な用途のテンプレートが用意されていて非常にわかりやすいです。

https://astro.build/themes/

Portfolioなのでなるべくシンプルに、カラーテーマを手軽に変更できて、コンテンツを.mdxで記述したかったので、Astro Modern Personal Websiteというテーマを使わせていただきました。

必要ない情報を落とす

今回選んだテーマでは、blog だったり store ページにも対応できるように、様々なページが実装されています。

今回はあくまでもポートフォリオなので、自分の経歴とか自分の成果物がわかるように不必要な情報を実装から落としていきます。

下記は修正した実装の例です。

  • サイドバーのリンクを修正する
  • store ページを消す

自分の機能を追加する

必要ない情報落としたら、次にデザインはダメダメでも自分がいれたい機能をいれていきました。

下記は入れた機能の実装例です。

  • ちょっと SPA っぽくしたかったのでアンカーリンクでページの特定位置に飛べるようにする
  • 画像をクリックしたときにモーダルウィンドウで画像を表示する
  • Card コンポーネントの中でいい感じに位置調整されてリストを表示する

デザインをいい感じにする

daisyUI を使用しているので、基本触ることがなければいい感じのテーマを使ってくれますが、やっとこの段階で簡単な位置調整などをしていきます。

  • Card コンポーネントの表示位置の修正
  • navbar の表示位置の修正
  • 各コンポーネントにおけるテキストのサイズの修正
  • 幅や高さの調整
  • モバイルサイズに対応するように修正

ここまででなんとなく Astro のポートフォリオサイトが完成しているので、様々な自動化ツールを導入して DX を向上させていきましょう。

PlayWright の導入

PlayWrightは、Web テスト自動化のためのテストフレームワークです。

Astro の公式ドキュメントには、

Astro supports many popular tools for unit tests, component tests, and end-to-end tests including Jest, Mocha, Jasmine, Cypress and Playwright.

とあるように、Jest も使えるようですが、公式ドキュメントでは PlayWright のテスト例が記載されているので今回は PlayWright を導入します。

PlayWright の公式通りに Astro プロジェクトでnpm init playwright@latestを実行してplaywright.config.tsを生成しましょう。

基本的には Jest でやっていたような Snapshot Test を実行していきたいのですが、PlayWright では Web 画面の差分を比較するための機能が存在しているので、もはや Snapshot Test ではなく VRT(Visual Regression Testing)を導入することにしました。

とりあえずpackage.jsonにテスト用のスクリプトを追加しておきます。

package.json
  "scripts": {
+    "e2e:all": "playwright test",
+    "e2e:chromium": "playwright test --project=chromium",
  }

そしてAstro の公式通りに、Astro プロジェクトにテストを設定していきます。設定としてやったことは下記の部分ですね。baseURLを設定して、テスト時にlocalhost:3000でテスト用のローカルサーバーが立ち上がるようにしただけです(これも公式通りですね)

playwright.config.ts
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: "http://localhost:3000/",
  },
+  webServer: {
+    command: "npm run preview",
+    port: 3000,
+    reuseExistingServer: !process.env.CI,
+  },

VRT の実施

数行テストコード書くだけで勝手にやってくれるのもいいですね。

index.spec.ts
+ test("first landing page", async ({ page }) => {
+   await page.goto("/");
+   await expect(page).toHaveScreenshot();
+ });

テストを実行するとこんな感じの画像がtests/index.spec.ts-snapshots/のディレクトリに作成されます。

first-test

テストをわざと失敗させるとわかるのですが、PlayWright は テスト結果を 確認する web アプリがついているので、めちゃわかりやすいな〜って思います。

例えば、portfolio サイトの文言をちょっと編集して、ビルドしてテストしてみると、下記みたいな感じで落ちて、どこが失敗したのか画像の diff が見れるようになります。

playwright失敗時

playwright失敗時の差分の比較UI

playwright失敗時の差分の比較画像

レスポンシブデザインに対応するために、画面が一定値よりも小さいときのテストも実行するようにしましょう。

index.spec.ts
 test("first landing page", async ({ page }) => {
   await page.goto("/");
   await expect(page).toHaveScreenshot();
 });
+ test("first landing page with limit width", async ({ page }) => {
+  await page.setViewportSize({ width: 1023, height: 1000 });
+  await page.goto("/");
+
+  await expect(page).toHaveScreenshot();
});

下記の結果になります。

test-results-in-chromium-limit-width

PlayWright の VRT を導入して気をつけるポイント

これは自分が体感した気をつけるべきポイントですが、VRT をやる場合は画像ピクセル単位での diff を行うので、見た目でなにか少しでもズレたらそれが diff としてテストが落ちる場合が多くあったというところです。

具体的に自分が詰まったポイントと解決した方法は下記になります。

どこで失敗したか どうしたか
環境によってフォントが異なる場合 web フォントなど統一したフォントを使用するようにした
スクロールバーの表示/非表示の場合 スクロールバーを表示しないようにした
ブラウザの環境が異なる場合 まずは自分がよく使う chromium で pass するようにした
gif などアニメーションが再生される場合 gif などブラウザの css アニメーションと関係ない部分では、テスト時にマスキングしてその部分を静止画として映すようにした
どれだけ環境を統一してもどこかしらピクセルレベルで差分が出てくる場合 しきい値を設定して多少の差分は良しとする

下記少しハマった部分について深堀りしています。

環境によってフォントが異なる場合

playwright 実行時に日本語のフォントだったり絵文字だったりが表示されなくなったりしてしまうので、必要なフォントを導入すれば解決します。

今回は web フォントを道入して事なきを得ました

Astro の公式ドキュメントに web フォントの導入方法が記載されているので、FontSourceを利用して web フォントを道入しました。

使い方は簡単で、ベースとなるレイアウトファイルに style を直接追記するだけで使用できます。

BaseLayout.astro
import { SITE_TITLE, SITE_DESCRIPTION } from "../config";
+ import "@fontsource/sawarabi-gothic";

<!DOCTYPE html>
<html lang="en" data-theme="dark">
  <head>
    <BaseHead title={title} description={description} image={image} />
  </head>
  <body>
+    <style>
+      body {
+        font-family: "Sawarabi Gothic", sans-serif;
+      }
+    </style>

スクロールバーの表示/非表示の場合

toHaveScreenshot()のオプションでfullPageというのがあるのでこれを使えばページ全体の snapshot を撮ることができるっぽいですが、今回の作成物に関しては playwright によるスクロールがうまく動きませんでした。(実装の問題だと思います。。

そうなったときに、スクロールバー(playwright がスクロールしようと頑張った跡)が snapshot に記録されてしまい、テスト時にそれが表示されてたり表示されてなかったりしてしまったので、fullPageオプションはfalseにしてそもそもスクロールしないようにしていました。(デフォルトはfalseでした)

gif などアニメーションが再生される場合

例えば css のアニメーションを消すときはtoHaveScreenshot()のオプション、animationsを調整可能ですが(デフォルトではfalse)、gitアニメーションなどは再生されてしまいます。

そのため、動きの発生する要素を指定してテスト時にマスキングすることで、VRT行うときは常に一定の表示でテストできるようになります。

index.spec.ts
test("projects button", async ({ page }) => {
  await page.goto("/");

  await page.click("//li/a[contains(@href, 'projects/1')]");

+  const maskedElement = await page.locator("//img[contains(@src, '.gif')]");
  await expect(page).toHaveScreenshot({
+    animations: "disabled",
+    mask: [maskedElement],
  });
});

VRT時にマスキングされた部分
gif画像をマスキングした状態のsnapshot

どれだけ環境を統一してもどこかしらピクセルレベルで差分が出てくる場合

なんか実行しても下記みたいな感じで数ピクセル差分が出てきてしまうときがあります。

数ピクセル違いがあるだけで失敗する例

「数ピクセルであればもはやそれは同じ画像でしょ」ということで、VRT の際にどれだけの差分であれば許容するかしきい値を設定することができます。

index.spec.ts
test("first landing page", async ({ page }) => {
  await page.goto("/");
+  await expect(page).toHaveScreenshot({ threshold: THRESHOLD });
});

huskyの導入

huskyは、gitコマンド実行時の動作をhookして様々なカスタムスクリプトを実行するためのツールです。

なので、こいつを使ってRepositoryにpushする前にPlayWrightのテストを実行して、passすればpushできるようにしていきます。

導入は公式に従って、git hooksを有効化していきます。

npm install husky -D
npx instal husky
npm pkg set scripts.prepare="husky install"

後はpre-push時に動作させるスクリプトを定義してあげるだけで使えるようになります。簡単。

npx husky add .husky/pre-push "npm run build; npm run e2e:all -- --reporter=list;"

PlayWrightではテスト結果のreporterを選択できます。(参考)

pre-push走ったときに毎回html上での報告を見て。。。ってやるのも面倒なので、よくあるテストツールみたいに落ちたところだけをlineでreportしてくれるようにオプションを設定しています。

まとめ

UIも新たにいい感じにReactベースのportfolioからAstroに移行できました!

また、既存の自動化ツールやフロントエンド技術との親和性も高く、ドキュメントも充実している印象を受けたので、
webページのみならずアプリケーションにもAstroを道入してみようかなって思いました。

Lighthouse でパフォーマンス計測してみましたがいい感じになっていたり、MPA だとしてもなかなかヌルヌルな感じで体感よかったです。

react-portfolio
もともと作っていたReactのPortfolio

astro-portfolio
今回Astroで作ったPortfolio

紹介残しとしては、n番煎じのgithub pagesへの自動デプロイがあるのでまたの機会に。

感想

フロントエンドはあくまでも趣味なのであまり積極的にキャッチアップは行っておらず、
気分転換に触ってみるか〜という程度でしたが、Astroいい感じに楽しいですね。

ここでは試していないのですが様々なフロントエンドフレームワークを輸入できるらしいし、CSS in JSよりも簡単にstylingができるのが何より楽しいです。

今回初めてTailwindだったりdaisyUIに触れましたが、今までCSS in JSしかやってこなかった自分からしてみれば新鮮でした。

そのうちAstro NativeとかAstro GUI tools with FigmaみたいなのとかStorybook for Astroみたいなものが出てくれないかな〜って思いました。

まだまだ未熟者なので変な箇所があれば教えてくださると嬉しいです👐

Discussion