Docker におけるブラウザプラットフォーム、グラフィックス、Playwright

最終更新: 2022-08-21 22:50:08 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

ブラウザ自動化では、ページを静的な文字列として扱うのではなく、ドキュメントの読み込み、レイアウト、ネットワーク、レンダリングをテストがモデル化すると、適切に機能します。

ブラウザと DOM を計測する #

ブラウザの各種サイズは、それぞれ異なる問いに答えます。

API計測対象
window.innerWidth と innerHeightレイアウトビューポート。描画されたスクロールバーを含む
window.outerWidth と outerHeightブラウザウィンドウの境界
screen.width と screen.heightCSS ピクセル単位の画面サイズ
screen.availWidth と availHeightオペレーティングシステムが予約した領域を除く画面領域
element.clientWidth と clientHeightコンテンツボックスとパディング。ボーダーとスクロールバーを除く
element.scrollWidth と scrollHeightスクロール可能なコンテンツの範囲
element.offsetWidth と offsetHeightボーダーボックスのサイズ。整数ピクセルに丸められる
element.getBoundingClientRect()ビューポート座標でのボーダーボックスのジオメトリ。小数を保持する

レイアウトボックスを持たないインライン要素では、clientWidth は 0 です。ルート要素では、クラシックなスクロールバーを除いたレイアウトビューポートを表します。getBoundingClientRect() のビューポート座標とドキュメント座標を、スクロールオフセットを加えずに混在させないでください。

正しい HTML 構造とリソースヒントを使用する #

<link> は外部リソースとの関係を表します。rel 属性は、標準化されたリンクタイプを空白で区切った集合です。

html
<link rel="stylesheet" href="/site.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">

preconnect はトランスポート接続を早期に確立します。ページが接続するオリジンに対してのみ使用してください。スタイルシート、フォント、スクリプトをダウンロードするものではありません。

<meta> は、文字セット、ビューポート、説明、HTTP 相当ディレクティブなどのドキュメントメタデータを提供します。後続のバイト列をパーサーが正しくデコードできるよう、<meta charset="utf-8"> は head の早い位置に置いてください。

<noscript> は、スクリプトが無効または非対応の場合のフォールバックコンテンツを提供します。解析動作は head と body のどちらに現れるかによって異なるため、生成されたマークアップを HTML バリデーターとブラウザでテストしてください。

<p> 要素は別の段落を含められません。HTML パーサーは内側の段落の前に外側の段落を閉じるため、DOM はソーステキストから変化します。テンプレートコードでは、代わりに兄弟要素または意味的なコンテナーを構築してください。

html
<div class="notice">
  <p>First paragraph.</p>
  <p>Second paragraph.</p>
</div>

スクリプトの読み込みとモジュール依存関係をモデル化する #

async も defer も指定されていないクラシックスクリプトは、ブラウザが取得と実行を行う間、HTML の解析をブロックします。そのため、スクリプトの配置によって利用可能なノードとグローバルが決まります。

html
<script src="/jquery.js"></script>
<script src="/plugin-that-needs-jquery.js"></script>

2 番目のスクリプトが 1 番目によって作成されたグローバルを使用するため、この順序が必要です。依存するスクリプトを提供元より前に移動すると、参照エラーが発生します。

形式パーサーのブロック実行順序DOM の利用可能性
クラシックスクリプトありドキュメント順タグより前に解析されたノード
defer取得はノンブロッキング解析後のドキュメント順解析済みの完全なドキュメント
async取得はノンブロッキング完了したものから先に実行解析完了とは連動しない
type="module"取得はノンブロッキングモジュール依存関係の順序デフォルトで遅延実行

defer は順序が必要なアプリケーションスクリプトに適しています。async は、ドキュメント内の位置や他のスクリプトに依存できない分析スクリプトなど、独立したスクリプトに適しています。モジュールはデフォルトで遅延実行され、import の依存関係を解決し、独自のスコープを持ち、モジュール URL ごとに一度だけ実行されます。クラシックコードを解析後に実行する必要がある場合は DOMContentLoaded を使用し、画像、スタイルシート、サブフレームも関係する場合にのみ load を使用してください。

動的に追加されたスクリプトは、デフォルトで非同期です。クラシックスクリプトを意図的に直列化するローダーでのみ、挿入前に script.async = false を設定してください。デプロイ上の制約が許す場合は、カスタムローダーよりもネイティブ ES モジュールと明示的な import 依存関係を優先してください。

制約からレスポンシブ CSS を設計する #

CSS ボックスモデルには、コンテンツ、パディング、ボーダー、マージンがあります。box-sizing は、宣言した幅と高さにパディングとボーダーを含めるかどうかを変更します。

css
*, *::before, *::after {
  box-sizing: border-box;
}

border-box では、width: 20rem はボーダーボックスの幅です。初期値の content-box では、パディングとボーダーが描画幅に加算されます。ユニバーサルなボーダーボックス規則により、コンポーネントの寸法を組み合わせやすくなります。

Flexbox は一次元の並びをレイアウトします。Grid は明示的な行と列をレイアウトします。ツールバーや直線的なコンテンツフローには flex を使用し、項目を両軸にわたって整列させる必要がある場合は grid を使用してください。

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: 1rem;
}

minmax 式は、狭いビューポートでは単一列を許可しながら、読みやすい列を維持します。gap はレイアウトコンテナーに属し、マージンの相殺や first/last-child の例外を回避します。

メディアクエリは、環境機能に基づいてルールを選択します。

css
@media (width >= 48rem) {
  .sidebar-layout {
    grid-template-columns: minmax(16rem, 1fr) minmax(0, 3fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 1ms; transition-duration: 1ms; }
}

ビューポートによってデザインで利用可能な空間が変わる場合は、ビューポートブレークポイントを使用してください。同じページ内の異なる幅の領域に再利用可能なコンポーネントが現れる場合は、コンテナクエリの方が適しています。

CSS の長さには異なる基準ボックスがあります。rem はルートのフォントサイズ、em は現在のフォントサイズを参照し、パーセンテージはプロパティに依存し、vw と vh はビューポートの寸法を参照します。コンテンツが必要とする場合は、フォントスタックで非ラテン文字のグリフ対応範囲を維持してください。

css
.cjk-sample::before {
  content: "漢";
  font-family: "Noto Sans CJK SC", sans-serif;
}

このグリフはフォント対応範囲のテストであり、本文コンテンツではありません。

ユーザーの色設定を尊重する #

両方のカラースキームへの対応を宣言し、意味を持つ色変数を定義してください。

css
:root {
  color-scheme: light dark;
  --surface: Canvas;
  --text: CanvasText;
  --accent: #2867c8;
}

@media (prefers-color-scheme: dark) {
  :root { --accent: #8ab4f8; }
}

prefers-color-scheme は、オペレーティングシステムまたはユーザーエージェントの設定を反映します。アプリケーションが制御するテーマセレクターは、ルート要素に属性を設定して変数を上書きできますが、保存済み設定、明示的なユーザー選択、システム設定の間に明確な優先順位を維持する必要があります。

埋め込み SVG や iframe は別のドキュメントです。親ドキュメントの CSS カスタムプロパティはその境界を越えません。埋め込みリソースには独自のスタイル、または意図的なメッセージングとテーマ機構が必要です。

予測可能なフォールバックでフォントを読み込む #

書体ファミリーには、レギュラー、イタリック、ボールド、ボールドイタリックなどの関連するフェイスが含まれます。グリフは文字の視覚的な表現です。ブラウザは font-family を順にたどって、文字ごとにフォントを選択します。

css
body {
  font-family: "Example Sans", system-ui, sans-serif;
}

あるフェイスにグリフがない場合、後ろにあるファミリーからフォールバックが選ばれます。名前に空白が含まれるファミリー名には引用符が必要です。

@font-face は、ファミリー名とダウンロード可能なリソースを関連付けます。

css
@font-face {
  font-family: "Example Sans";
  src: url("/fonts/example-sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
}

WOFF2 は、OpenType フォントデータ用の圧縮 Web フォントコンテナーです。可変フォントは wght、wdth、opsz、ital などの軸を公開できます。宣言する範囲は、サイトが配布するフェイスに限定してください。font-display: swap は Web フォントの読み込み中にフォールバックテキストを表示するため、メトリクスが互換するフォールバックを選ぶか、フォントメトリクスのオーバーライドを使用してレイアウトシフトを減らしてください。

window.queryLocalFonts() には権限が必要で、ブラウザの対応も限定的です。ローカルフォントの列挙にサイトのレンダリングを依存させないでください。

Canvas を正しくスケーリングする #

HTML の width と height 属性は、Canvas の描画バッファをデバイスピクセル単位で設定します。CSS の width と height は表示ボックスを設定します。400×800 の描画バッファを異なる CSS サイズで表示すると、ブラウザはビットマップをスケーリングします。

高密度ディスプレイで鮮明に描画するには、CSS ボックスとデバイスピクセル比からバッキングストアのサイズを決めます。

js
const rect = canvas.getBoundingClientRect();
const scale = window.devicePixelRatio;
canvas.width = Math.round(rect.width * scale);
canvas.height = Math.round(rect.height * scale);

const context = canvas.getContext("2d");
context.setTransform(scale, 0, 0, scale, 0, 0);

リサイズ後に描画する前に、変換をリセットしてください。メモリ使用量が過大になる大きな Canvas では、スケールを上限設定します。

CSS 仕様とアイコンを追跡する #

CSS は単一のモノリシックなリリースではなく、モジュール群です。CSS スナップショットは、ある時点で成熟したモジュールの一貫した集合を記述しますが、個々のモジュールは独立して進展します。名称付き CSS レベルから対応状況を推測せず、モジュールの機能ステータスとブラウザ対応を確認してください。

アイコンには、アクセシビリティ、スタイリング、読み込みについて別々のトレードオフがあります。

形式長所制限
アイコンフォントcolor とテキストサイズを継承するモノクロのグリフ制約とフォント読み込み
Unicode 文字追加アセットが不要セマンティクスと外観がフォントやプラットフォームによって異なる
インライン SVGアクセシブルな構造とパーツ単位のスタイリングHTML またはコンポーネントの複雑さが増す
外部 SVG またはラスター画像独立したアセットとしてキャッシュ可能内部の詳細をスタイリングするには明示的な設計が必要

アイコンだけのコントロールには、アクセシブルな名前を使用してください。

html
<button type="button" aria-label="Search">
  <svg aria-hidden="true" viewBox="0 0 24 24">...</svg>
</button>

aria-hidden="true" は、ボタンがテキストまたは aria-label によってアクセシブルな名前を提供する場合にのみ、装飾用 SVG に付けてください。

HTTP で動画を配信する #

ブラウザはシーク可能なメディアに対してバイト範囲を要求します。

http
GET /movie.mp4 HTTP/1.1
Range: bytes=32243712-32955076
If-Range: "entity-tag"

準拠した範囲レスポンスのステータスは 206 Partial Content で、Content-Range ヘッダーを含みます。Accept-Ranges: bytes、安定したバリデーター、正しいメディアタイプを提供してください。範囲を無視するサーバーは 200 を返せますが、クライアントがファイル全体を必要とする可能性があるため、シークのコストが高くなります。

MPEG-DASH は、MPD マニフェストに記述されたメディアセグメントにプレゼンテーションを分割します。クライアントは異なるビットレートまたは解像度の表現を選択し、帯域幅の変化に応じてセグメント選択を変更します。切り替えに対応したキーフレーム境界がセグメントに必要です。セグメント境界をまたぐ長い GOP を 1 つエンコードすると、独立デコードが妨げられます。

HLS もプレイリスト形式で同じ適応型セグメント原則に従います。ブラウザの対応範囲は異なるため、すべてのマニフェストを <video src> で消費できると仮定せず、対象プラットフォームに基づいてプレーヤーとパッケージ形式を選択してください。

TypeScript で列挙値をモデル化する #

固定された集合のいずれかを受け入れる API パラメーターに、数値の TypeScript enum は必須ではありません。文字列リテラルユニオンは、通信上の値を保持します。

ts
type Status = "queued" | "running" | "failed";

function retry(status: Status): boolean {
  return status === "failed";
}

実行時の反復処理と単一の信頼できる情報源が必要な場合は、定数オブジェクトを使用します。

ts
const Status = {
  queued: "queued",
  running: "running",
  failed: "failed",
} as const;

type Status = typeof Status[keyof typeof Status];

数値を指定するプロトコルには数値 enum が適する場合もありますが、逆マッピングと生成される JavaScript は無償ではありません。より豊富な定義済み値は、判別子と明示的な振る舞いを持つオブジェクトとしてモデル化し、ビジネスロジックを enum メンバーに押し込まないでください。

ダウンロードとインライン表示のブラウザ動作を説明する #

HTTP レスポンスは、ブラウザにコンテンツのダウンロードを要求できます。

http
Content-Type: application/pdf
Content-Disposition: attachment; filename="guide.pdf"

attachment はダウンロード動作を要求し、inline は対応するビューアーでの表示を許可します。ブラウザ、ユーザー設定、コンテンツタイプ、サンドボックスポリシー、セキュリティヘッダーが最終的な動作に影響する可能性があります。X-Content-Type-Options: nosniff はタイプスニッフィングを防止し、正しい Content-Type を重要にします。レスポンスヘッダーによってすべてのブラウザで同一の UI 動作になると仮定せず、ブラウザ自動化を通じてダウンロードをテストしてください。

GPU パイプラインを通してフレームをレンダリングする #

アプリケーションは、WebGL、WebGPU、Vulkan、Metal、Direct3D などの API を通して、バッファー、テクスチャ、シェーダープログラム、描画コマンドを送信します。その後、ドライバーと GPU の処理は概念的なパイプラインに従います。

  1. 頂点シェーダーが各頂点を変換し、varying 属性を生成する。
  2. プリミティブアセンブリが頂点を線、三角形、点にまとめる。
  3. クリッピングがクリップ空間外のジオメトリを除去または分割する。
  4. ラスタライズがサンプル位置を覆うフラグメントを生成する。
  5. フラグメントシェーダーがフラグメントごとの出力を計算する。
  6. 深度、ステンシル、ブレンディング処理が出力をフレームバッファーと合成する。

クリップ空間の座標は、透視除算の前に同次クリップ境界と比較されます。クリップ平面を横切る三角形は分割されるため、フラグメントシェーダーが可視範囲外のクリップ済みプリミティブのカバレッジを受け取ることはありません。

GPU インターフェースは、CPU と GPU の境界を越えてデータを移動させます。静的ジオメトリはデバイス常駐バッファーに保持し、動的データは上限付きの割り当てで更新し、同期的な読み戻しは避けてください。読み戻しによって CPU と GPU の進行が同期する必要が生じるためです。

カメラを配置し、ジオメトリをシェーディングする #

カメラはワールド座標をビュー座標に変換し、次にクリップ空間へ投影します。透視カメラは遠くのオブジェクトを画面上の小さな領域に写し、正投影カメラは深度にわたって平行線と見かけのサイズを維持します。一般的な Three.js の規約では、(0, 0, 0) にあり、向きが単位行列のカメラは負の Z 方向を見ており、正の Y が上です。

ランバート拡散照明は、つや消し表面をモデル化します。

text
diffuse = albedo × lightColor × max(dot(normal, lightDirection), 0)

内積は、表面法線と光方向の間の余弦を表します。Blinn-Phong は、光方向と視線方向の中間ベクトルを使用して経験的なスペキュラー項を追加します。どちらのモデルでも法線を正しく変換する必要があります。非一様スケーリングには、モデルビュー行列の逆転置行列を使用してください。

Three.js のカメラとマテリアルは、これらの概念をより高いレベルで提供します。カメラ位置を設定し、camera.lookAt(target) を呼び出し、アスペクト比を変更した後に投影行列を更新し、シーン更新後にレンダリングしてください。デフォルトのカメラ向きがカスタム座標系に適合すると仮定しないでください。

透明度と色を合成する #

標準の source-over アルファ合成は、ソースとデスティネーションを結合します。

text
out.rgb = source.rgb × source.a + destination.rgb × (1 - source.a)
out.a   = source.a + destination.a × (1 - source.a)

この式は、ソース RGB が premultiplied であることを前提とします。straight-alpha アセットには、レンダラーが期待する変換またはブレンド係数が必要です。ガンマエンコードされた sRGB ではなく、線形光空間でブレンドしてください。エンコードされた値を算術処理すると、暗くなりすぎたりグラデーションが不正確になったりするためです。

人間の色知覚は、重なり合う波長範囲に対する錐体細胞の応答から始まります。色空間は数値座標を定義された基準系に対応付けます。空間を伴わない座標は曖昧です。sRGB は原色、白色点、伝達関数、エンコーディング規約を定義します。照明または補間の前に sRGB を線形値へ変換し、その後、表示用にエンコードしてください。

HSL と HSV は便利な編集座標ですが、知覚的に均一な空間ではありません。色相、彩度、明度の数値を等しく変化させても、知覚される変化は等しくなりません。物理的な意味を持つブレンドのグラデーションには、線形 RGB を補間してください。知覚される明度を中心に設計したグラデーションには、OKLab などの知覚空間を検討し、対象の色域へ戻す際には色域外の色を意図的にクリップまたはマッピングしてください。

マゼンタは、単一のスペクトル波長を持たず、長波長と短波長に対する錐体応答が同時に生じることで作られる知覚色です。加法混色は 1 つの波長に平均化されず、視覚系が錐体からの複合信号を解釈します。

コンテナーで Playwright を実行する #

Playwright には、ブラウザーバイナリとオペレーティングシステムのライブラリが必要です。公式イメージのタグをプロジェクトの @playwright/test のバージョンに固定してください。

dockerfile
FROM mcr.microsoft.com/playwright:v1.58.2-noble

WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN chown -R pwuser:pwuser /work
USER pwuser
CMD ["npx", "playwright", "test"]

公式イメージには、対応するブラウザーと依存関係が含まれています。イメージとパッケージのバージョンは同時に更新してください。init プロセスを使用してビルドと実行を行います。

sh
docker build -t example-playwright-tests .
docker run --rm --init --ipc=host example-playwright-tests

--init はブラウザーの子プロセスを回収します。Chromium は --ipc=host による大きな共有メモリ領域の恩恵を受けますが、信頼できる環境でのみ使用してください。カスタム Node ベースイメージでは、イメージのビルド中にブラウザーの依存関係と選択したブラウザーの両方をインストールする必要があります。

dockerfile
FROM node:22-bookworm
WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci && npx playwright install --with-deps chromium
COPY . .
CMD ["npx", "playwright", "test"]

コンテナーの起動ごとにブラウザーをダウンロードしないでください。レポート、トレース、スクリーンショット、動画は CI アーティファクトとして永続化してください。失敗はテスト環境に依存することが多いためです。

無限ループなしで仮想化リストを自動化する #

仮想化リストは、表示される項目のノードを作成し、スペーサーによって全体のスクロール範囲を表します。DOM に存在しない項目がデータセットには存在する可能性があるため、ロケーターはスクロールを行い、レンダーターンを待機する必要があります。

検索に上限を設け、進捗がないことを検出してください。

ts
const scroller = page.locator('ul[role="listbox"] > div');
const option = page.getByRole("option", { name: targetAddress, exact: true });

for (let attempts = 0; attempts < 500; attempts += 1) {
  if (await option.count()) {
    await option.click();
    break;
  }

  const moved = await scroller.evaluate((element) => {
    const before = element.scrollTop;
    element.scrollBy(0, Math.max(1, element.clientHeight * 0.8));
    return element.scrollTop !== before;
  });
  if (!moved) throw new Error(`Option not found: ${targetAddress}`);
  await page.waitForTimeout(0);
}

waitForTimeout(0) はブラウザのイベントループに制御を返し、スクロールリスナーまたはフレームワークのレンダーによって仮想化されたノードが更新されるようにします。実運用のテストでは、アプリケーションに存在する場合はデータ API または意味的な検索コントロールを優先してください。ピクセルに近いスクロールは、描画されたインターフェースを通じてテストする必要がある動作のフォールバックです。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)