Chrome DevToolsで学ぶWebページの調査実践

画面の違いを記録し、HTML、CSS、通信、操作のどこを調べるかを判断するための実習記事です。 掲載画像は、この原稿用に作成したローカル教材をmacOSのChromeで操作して撮影しました。 DevTools本体の撮影ではありません。DevToolsの操作は公式資料に基づく追試手順として説明します。 実習教材をダウンロード(ZIP)し、解凍したフォルダーのREADMEに従って起動してください。

1. 調査を始める前に症状を記録する

Webブラウザー上で表示の崩れや意図しない挙動に直面したとき、あて推量でスタイルを書き換えると原因の特定が難しくなります。最初に、操作の手順と画面の変化をメモします。

観測と推測を分ける四段階の作業表

調査手順を観測、仮説、変更、再確認という四つの段階に整理した作業表を用います。作業表の各項目には、調査者が行う判断の役割を明確に割り当てます。

  • 観測:操作を行った環境、実行した手順、画面に現れた具体的な文字列や寸法の変化など、客観的な事実のみを書き出します。
  • 仮説:観測された事実の差分を生み出していると考えられる構造的な理由を、一つだけ推定します。
  • 変更:仮説の妥当性を確かめるために、最小単位のスタイル指定や属性値の書き換えを一度に一箇所だけ行います。
  • 再確認:変更を加えたあとの画面を再度観測し、仮説どおりの結果が得られたか、あるいは新たな差分が生じたかを記録します。

各段階を一行ずつ記録に残しながら進めることで、手詰まりになった場合でも直前の確定情報まで安全に作業を巻き戻せます。

調査用ローカル教材の構成と表示の確認

実習では、手元の端末でHTTPサーバーを立ち上げ、HTMLファイルとJSONファイルの二つを配信して動作させるローカル教材を使用します。教材の起動手順や前提環境の配置については、配布物に含まれる README の案内に従って準備を済ませてください。

ページ下部のフッターにも明記されているとおり、この教材に対してブラウザー上で行ったDOMやスタイルの書き換えは、閲覧中の一時的なメモリ上にのみ保持されます。ブラウザーの再読み込みを行えば、スクリプトが読み込まれた直後の初期状態に復帰します。外部のサーバーへフォーム情報や閲覧ログを送信する処理も含まれていません。

教材のページを開くと、URLの末尾に付与されたクエリパラメータ ?case=overview に応じて初期画面が描画されます。macOS上のChromeでこのローカル教材を開いた際の初期表示が、次の画像です。

教材の初期画面。表示、通信、操作の三つの観察領域

画面上には「見え方と原因を分けて記録する」という表題とともに、表示、通信、操作という三つの観察領域を示したカードが横並びに配置されています。この画像自体はChromeのDevToolsそのものを撮影したものではなく、調査の対象となる教材ページ自体の初期状態を示した記録です。調査を始める際には、まずこのように「操作を何も加えていない基準の画面」を控え、何が書かれており、どの枠線がどこに配置されているかを把握します。

Chrome DevToolsの起動と配置の違い

Webページの内部状態を観察するための標準的な機能が、Chromeに統合されているDevToolsです。公式資料の Chrome DevTools の概要と開き方 にあるように、DevToolsはページの要素を右クリックして検証を選ぶか、ブラウザー右上のメニューから起動できます。ウィンドウの配置は利用環境に合わせて変更可能です。

DevToolsは手元の画面で観測した差異の理由を調べるための補助具であり、最初に見るべき対象は常にWebブラウザーがレンダリングしている教材ページそのものです。

調査メモの作成例と記録の粒度

客観的な事実に基づいた調査メモを作成する習慣をつけます。調査メモに書くべき内容は主観的な印象ではなく、第三者が同じ手順を踏んだときに同じ挙動を確認できる客観的な記述です。教材の初期表示を確認した段階では、次のような形式で記録を残します。

[調査メモ: 初回観測]
対象URL: http://127.0.0.1:9033/demo/?case=overview
観測環境: macOS, Google Chrome
初期状態の表示:
- ヘッダーに「Web調査の練習室」の文字列が存在する。
- 画面中央に「01 / 調査の入口」という小さなタグと「見え方と原因を分けて記録する」という大見出しが表示されている。
- 大見出しの下に「表示」「通信」「操作」の3枚のカードが横並びで配置されている。
- カードの下部には「Elements → Styles」「Network」「Accessibility」という対応パネルの文字列がコード形式で確認できる。
- ページ最下部に「変更はこの教材の表示に限ります。再読み込みで初期状態に戻ります。」のフッター文がある。

このメモには個人の解釈や推測による原因論を交えず、「3枚のカードが横並びで配置されている」という具体的な配置の事実だけを記載しています。記録の粒度を視覚情報とテキスト情報に限定しておくことで、後続のステップで変更を加えた際、どの部分が意図どおりに変化したかを明確に対比できます。

症状の切り分けを行うための着眼点

問題の所在を突き止めるには、事象を適切なカテゴリに腑分けして考える必要があります。教材の「01 / 調査の入口」の画面で提示されているとおり、調査の着眼点は大きく三つに分類できます。

  • 表示の切り分け:要素の寸法、余白の広さ、テキストの折り返しなど、純粋な視覚的配置に関する問題です。CSSのプロパティ値やボックスモデルの計算規則が関与している可能性を検討します。
  • 通信の切り分け:外部から取得すべきデータが届いていない、または通信自体が失敗しているといった入出力の問題です。リクエストされたURLの正しさやHTTPステータスコードを調べる対象とします。
  • 操作の切り分け:クリック、文字入力、Tabキーによる移動など、利用者のアクションに対するインターフェースの反応に関する問題です。イベントの発火やDOM要素の属性値の変化を確かめます。

例えば画面上の表示枠から文字がはみ出している場合、親要素のCSSに過剰な固定幅が指定されている可能性もあれば、通信で想定よりも長い文面が送られてきた可能性、あるいは操作によって動的にスタイルクラスが変更された可能性も考えられます。観察された一つの症状に対して、三つの領域のどこに構造的な理由があるのかを仮説として立て、一つずつ検証を重ねていく姿勢が求められます。

2. 初期HTMLと操作後のDOMを読み分ける

Webブラウザーは受信したHTMLを構文解析してメモリ上にDOM(Document Object Model)を構築し、その後のJavaScriptの実行や利用者の操作によってその木構造を書き換えます。 画面に見えている要素の正体を突き止めるには、送信されてきた静的なファイルの内容と、ブラウザー内部で変化した現在の構造を明確に分けて観察しなければなりません。

画面に現れる文字と初期コードの落とし穴

Webページ上の表示崩れや文言の不整合を調査する際、スクリプトによって作られる要素は、HTMLのマークアップとしては存在しないことがあります。文字列や生成処理はソースコードの検索でも探せますが、検索結果だけでは現在の表示状態を確定できません。 ソースコードそのものを探す作業と、動的に生成された現在の状態を調べる作業を切り離す必要があります。

本節で対象とする画面は、HTTPサーバーで配信されるHTMLファイルとJSONデータから構成される検証用教材の一部です。 ブラウザーで http://127.0.0.1:9033/demo/?case=dom にアクセスすると、以下のHTML文字列が読み込まれて領域内に展開されます。

<!-- demo/index.html の抜粋 -->
<section>
  <div class="tag">04 / 状態とDOM</div>
  <h2>操作のあとで増える要素</h2>
  <p>「詳細を表示」で説明要素を追加し、「詳細を閉じる」で取り除きます。</p>
  <button id="toggle" aria-expanded="false" aria-controls="detail">詳細を表示</button>
  <div id="detail"></div>
  <p class="note">HTMLファイルに書かれた初期状態と、操作後のDOMを区別して観察します。</p>
</section>

初期状態のコードでは <div id="detail"></div> の中身は空であり、子要素は含まれていません。 この状態でブラウザーに表示される外観が次の画像です。

詳細を表示する前。説明段落はまだない

画面上には「詳細を表示」というラベルのボタンが配置されているだけで、説明文の領域は見当たりません。 この外観は初期HTMLの構造と一致しています。

操作によって生じるDOMツリーの書き換え

教材に含まれるスクリプトでは、ボタンのクリックイベントに伴って次の処理が実行されます。

// demo/index.html の抜粋
if (mode === 'dom') {
  document.querySelector('#toggle').onclick = (event) => {
    const button = event.currentTarget;
    const open = button.getAttribute('aria-expanded') === 'true';
    button.setAttribute('aria-expanded', String(!open));
    button.textContent = open ? '詳細を表示' : '詳細を閉じる';
    const detail = document.querySelector('#detail');
    detail.replaceChildren();
    if (!open) {
      const p = document.createElement('p');
      p.className = 'result';
      p.textContent = 'この説明はボタン操作後にJavaScriptで追加されました。';
      detail.append(p);
    }
  };
}

利用者がボタンを押すと、aria-expanded 属性が反転し、表示テキストが「詳細を閉じる」に更新されます。 続いて detail.replaceChildren() で #detail の中身が一度空にリセットされた後、開く操作(!open)であれば新しい <p> 要素が生成されて挿入されます。 macOSのGoogle Chrome上でボタンをクリックしたあとの表示が次の画像です。

詳細を表示した後。説明段落と閉じるボタン

枠線付きの領域が出現し、「この説明はボタン操作後にJavaScriptで追加されました。」という段落が表示されます。 ボタンの表記も「詳細を閉じる」に変化します。 この段落の文字列はHTMLファイル内のJavaScriptにはありますが、初期の説明用コンテナには段落要素がありません。ブラウザーが保持するDOMを観察対象とすることで、スクリプトが生成した要素がツリーへ組み込まれた事実を把握できます。

Elementsパネルで現在の状態を直接確かめる

動的に変化する構造を調べるには開発者向けツールを活用します。 Chrome DevToolsドキュメントにあるとおり、Elementsパネルでは現在のDOMツリーの要素を直接検査できます。 ページのソース表示が生データを表示するのに対し、ElementsパネルはJavaScriptの変更が適用された現在のDOMを表示します。

教材ソースと操作後の画面から整理した変更点は次のとおりです。Elementsパネル自体は撮影していません。追試ではこれらの項目を照合してください。

観測対象 操作前(初期状態) 操作後(ボタンクリック後) 変化の意味と役割
button#toggle の textContent 詳細を表示 詳細を閉じる 次に行える操作を示す可視ラベル
button#toggle の aria-expanded "false" "true" 制御対象が展開されているかを示す支援技術向け属性
div#detail の子要素数 0個(空要素) 1個(<p class="result">) 説明コンテンツのノード追加
<p class="result"> のテキスト なし(要素自体が不在) この説明はボタン操作後に... スクリプトによって生成された文字列

外観上の段落追加だけでなく、aria-expanded 属性値の遷移も確認できます。

閉じる操作と反復追試で完全な往復を確認する

状態を切り替えるUIでは、閉じた状態に戻したときにDOMが復元されるかの追試も必要です。 今回の教材コードでは、開いた状態でもう一度ボタンを押すと open が真となり、ボタンの属性は "false" に、テキストは「詳細を表示」に戻ります。 コンテナ要素に対して引数なしで detail.replaceChildren() が呼び出されると、配下のすべての子ノードが消去されます。 if (!open) ブロックは実行されないため、#detail 配下は初期状態と同じ空の状態へ戻ります。

今回の教材では、ボタン操作によって次の3点を確認します。

  1. ボタンの表記が再び「詳細を表示」に戻っているか
  2. aria-expanded 属性が "false" に戻っているか
  3. #detail の中から <p> 要素が消去され、初期状態の空要素に戻っているか

仮に p.style.display = 'none' のようなスタイル指定で非表示にされた場合はDOM上に要素が残存しますが、本教材のように replaceChildren() でノードを破棄した場合はDOMツリーから要素が消失します。 ノードが消去されたのか、スタイルで非表示になっただけなのかを峻別することが重要です。

3. CSSの指定と使われた値を切り分ける

ブラウザーの表示が崩れたとき、スタイルシートに書いたプロパティを眺めるだけでは原因を特定できない場面があります。ソースコード上の記述はブラウザーに対する指定の候補であり、規則の衝突や継承、インライン指定などを経て最終的な描画値が決まります。

調査では、書かれたCSS宣言とブラウザーが適用している値を分けて観察します。CSS Cascading仕様には指定値、計算値、使用値という別々の段階があるため、DevToolsの表示をすべて同じ意味の値とみなさないようにします。Chrome DevToolsのElementsパネルには、この二つの段階を個別に点検するためのタブが用意されています。

StylesタブとComputedタブの役割を分担する

Elementsパネルの補助ペインに並ぶStylesタブとComputedタブは、CSSが要素に作用する工程の異なる側面を写し出します。

Chrome DevToolsの公式リファレンス(CSS features reference)では、StylesペインでCSS規則の宣言を調べ、Computedペインで最終的に計算・適用された値を調べられると案内されています。Stylesペインに表示される宣言の取り消し線を手掛かりにすることで、どの規則が上書きされたかを追跡できます。

Stylesタブは、選択中の要素に一致したセレクターと宣言のリストを表示します。各宣言の出自となったスタイルシートのファイル名や行番号、規則同士の優先順位関係が確認できます。取り消し線などの表示は上書きや無効な宣言を探す手掛かりになりますが、表示理由とプロパティの状態も確認してください。

これに対してComputedタブは、適用された値を確認するために使います。すべてのプロパティが同じ段階でpxに変換されるわけではありません。DOM要素が保持しているプロパティ値そのものを確認したいときは、Computedタブを参照します。

調査対象のHTML構造と初期のスタイル定義

本章で用いる教材ページ(http://127.0.0.1:9033/demo/)には、スタイルの適用関係を切り分ける構造があらかじめ組み込まれています。

<!-- demo/index.html の抜粋 -->
<style>
*{box-sizing:border-box}body{margin:0;background:#edf1f2;color:#182b34;font:17px/1.75 system-ui,sans-serif}
/* ...略... */
.box{width:320px;padding:24px;border:4px solid #177887;background:#dceff2;margin:16px 0}
</style>
<!-- demo/index.html の抜粋 -->
<section>
  <div class="tag">02 / 幅の内訳</div>
  <h2>同じ width でも外側の幅が変わる</h2>
  <p>指定は <code>width: 320px; padding: 24px; border: 4px solid</code> です。</p>
  <button id="contentBox">content-box にする</button>
  <button id="borderBox" class="secondary">border-box にする</button>
  <div id="box" class="box" style="box-sizing:content-box">カードの内容領域</div>
  <p id="measure" class="result"></p>
  <p class="note">枠線を含む幅を測っています。margin はこの幅に含まれません。</p>
</section>

教材の先頭では、全称セレクター * に対して box-sizing: border-box が宣言されています。しかし、<div id="box" class="box"> 要素にはインラインスタイルとして style="box-sizing:content-box" が直接指定されています。さらにクラス名 .box には width: 320px、padding: 24px、border: 4px solid #177887 が定義されています。

全称セレクターとインラインスタイルの競合を追う

以下は公式資料と教材ソースに基づく追試手順です。今回はDevToolsのパネル表示そのものを確認していません。

ElementsパネルのDOMツリーで <div id="box"> を選択してStylesタブを開くと、最上段に要素自身のインラインスタイルを示す element.style が表示されます。

element.style {
  box-sizing: content-box;
}

その下方にスクロールすると、クラスセレクター .box、さらに下方に全称セレクター * のブロックが現れます。全称セレクターの枠内にある box-sizing: border-box には取り消し線が引かれます。

CSSのカスケードと詳細度の規則により、要素タグの style 属性に書かれたインライン宣言が、この教材の通常のクラス指定と全称セレクターの指定より優先されます。important宣言や他のオリジンを含むカスケード全体をこの例だけで説明することはできません。全称セレクターに同じプロパティ名が存在していても、ブラウザーはインラインスタイルの box-sizing: content-box を採択します。

Stylesタブで取り消し線を確認後、Computedタブで box-sizing を検索すると、算出値として content-box が表示されます。Stylesタブで上書きの事実を確認し、Computedタブで現在有効な値を確かめる手順を踏みます。なお、本教材では画面上のボタン操作によってプロパティ値を切り替えて検証できます。

スタイル適用の確認手順

スタイルの適用を判断する際は、次の要因を確認します。

  1. 要素の選択:現在調査しているDOMノードが対象の要素そのものであるか。
  2. 詳細度とカスケード:より詳細度の高いセレクターやインラインスタイルによって上書きされていないか。
  3. プロパティの文脈依存性:インライン要素に対する width のように、要素の display 形式によってプロパティ自体が無視されていないか。

継承されるプロパティと継承されないプロパティ

ある要素に直接指定がなくても、親要素のプロパティ値が引き継がれる場合があります。本教材の demo/index.html では、body 要素に対して次の指定が行われています。

/* demo/index.html の抜粋 */
body {
  margin: 0;
  background: #edf1f2;
  color: #182b34;
  font: 17px/1.75 system-ui, sans-serif;
}

<div id="box"> 要素自体には color や font の指定がありませんが、画面上の文字には #182b34 とシステムフォントが適用されます。Stylesタブで <div id="box"> を選択すると、Inherited from body の下に上記の定義が表示されます。

一方で、box-sizing、width、margin、padding、border といったボックスモデルを構成するプロパティは初期状態では親要素から継承されません。

値の切り分け手順

確認順序 観察する対象 参照するタブ 判断する基準
1 選択中のDOMノード Elements ツリー 調べたい対象要素そのものがハイライトされているか
2 インラインスタイルの有無 Styles (element.style) style 属性で直接プロパティが上書きされていないか
3 当該プロパティの宣言行 Styles 宣言に取り消し線が引かれていないか
4 親要素からの継承 Styles (Inherited from...) プロパティが親から引き継がれたものか、直接の指定か
5 最終計算値 Computed 単位換算を含め、描画に使われている値は何か

4. ボックスモデルを数値で説明する

Webページのレイアウト崩れを調べる際、CSSで指定した数値とブラウザーが画面上に割り当てた幅の食い違いを解消するには、要素の大きさを構成する各領域を個別の数値に分解し、どのような規則で加算されているかを数式で確かめる必要があります。本章では、CSSのボックスモデルを構成する各層の役割を整理し、教材ソースの数値と教材画面の測定結果を照合しながら幅の計算根拠を解説します。

画面上の占有幅を構成する四つの領域

ブラウザーがHTML要素を描画する際、通常のCSSレイアウトでは、要素が生成する箱の寸法をもとに配置が決まります。この大きさと位置を決める仕組みがボックスモデルです。ボックスモデルは内側から順に、テキストや画像が配置されるコンテンツ領域、余白のpadding、境界線のborder、他の要素との距離を保つmarginの四つの領域で構成されます。

スタイルシートに記述した width プロパティがどの領域を制御するかを決定するのが box-sizing プロパティです。初期値の content-box と、広く使われる border-box のどちらを指定するかで、外見上の幅とCSSの指定値との対応関係が変化します。

教材の構造と二つの幅の現れ方

教材ファイル demo/index.html では、ボックスモデルの挙動を確認するための領域が次のように定義されています。なお、本教材はHTTPサーバー経由(例: http://127.0.0.1:9033/demo/)でHTMLとJSONの二つのファイルを配信して動作させます。

<!-- demo/index.html の抜粋 -->
<div id="box" class="box" style="box-sizing:content-box">カードの内容領域</div>
<p id="measure" class="result"></p>
/* demo/index.html の抜粋 */
.box{width:320px;padding:24px;border:4px solid #177887;background:#dceff2;margin:16px 0}

幅に width: 320px、余白に padding: 24px、枠線に border: 4px solid #177887、外側余白に margin: 16px 0 が宣言されています。ボタン操作によりインラインスタイルの box-sizing が切り替わり、getComputedStyle(box).boxSizing による計算値と box.getBoundingClientRect().width による描画幅が画面下に反映されます。

初期状態(content-box)の表示が次の画像です。

content-boxでは枠線を含む幅が376pxになった

計測結果には「box-sizing: content-box / 枠線を含む幅: 376px」と出力されます。width: 320px の指定に対し、枠線を含めた横幅は 376px に達しています。

376pxと320pxの内訳を計算で解明する

box-sizing: content-box において、width はコンテンツ領域の幅を指定します。左右のpaddingとborderは指定幅の外側に加算されます。

320 + 24 × 2 + 4 × 2 = 376

コンテンツ幅 320px に左右padding 48px と左右border 8px が加わり、合計 376px となります。

続いて教材画面の「border-box にする」ボタンを押した状態を確認します。

border-boxへ変更すると枠線を含む幅が320pxになった

表示は「box-sizing: border-box / 枠線を含む幅: 320px」へ変化します。box-sizing: border-box では、width がコンテンツ領域、padding、borderを合わせた枠線外側までの幅を指します。左右のborderとpaddingの合計 56px を差し引いた残りがコンテンツ幅です。

320 - (24 × 2) - (4 × 2) = 264

コンテンツ幅は 264px となります。

DevToolsでボックスモデルの各層を照合する

Chrome DevToolsのElementsパネルにあるStylesタブにはボックスモデルの図が表示されます。Chrome公式のCSS確認機能ガイドによると、Stylesペインのボックスモデル図からpadding、border、marginの各数値を確認でき、プロパティ値を一時編集して表示変化を試すことができます。

DevToolsで追試する場合は、コンテンツ幅とpadding、border、marginを別々に確認します。この教材ではpaddingは四辺24px、borderは四辺4px、marginは上下16px、左右0です。色や表示位置はUIの版により異なるため、ラベルと数値を照合してください。

marginの性質とgetBoundingClientRectの測定対象

marginは要素の境界線の外側に作られる間隔であり、要素自体の寸法には加算されません。背景色や枠線が描画されるのはborderの内側までで、margin領域には親要素の背景が透けます。

CSSOM View仕様で定義される getBoundingClientRect() メソッドは、要素のボーダーボックスを基準とした矩形の大きさとビューポートに対する相対位置を返すため、marginは含まれません。また、getBoundingClientRect() はCSSの transform による変形適用後の最終的な描画境界を取得します。例えば transform: scale(2) がある場合、レイアウト幅が 320px でも戻り値は変形後の 640px になります。実観測の限界として、スクリプト取得値がレイアウト上の寸法なのか変形後の描画境界なのかはコードと併せて識別する必要があります。本教材には transform 指定がないため、測定値 376px と 320px は純粋なボックスモデルの計算規則を反映しています。

数値と見え方を往復する調査手順

調査の過程を整理する手段として、次の表に、ソースから計算した値と画面に表示された測定値を分けて記します。

項目 content-box 設定時 border-box 設定時 備考
width のCSS指定値 320px 320px スタイルシートに記述された値
左 / 右 padding 24px / 24px 24px / 24px 内側の余白(合計 48px)
左 / 右 border 4px / 4px 4px / 4px 枠線の太さ(合計 8px)
計算上のコンテンツ幅 320px 264px テキストが配置される内側の幅
枠線を含む外見幅 376px 320px getBoundingClientRect() の測定値
外見幅の計算根拠 320 + 48 + 8 = 376 320 (固定) 差分 56px の扱いによる違い

演習:意図したレイアウトへの改善提案

全体の幅を 320px に保ちつつコンテンツ幅を 280px に広げる場合、paddingとborderの合計を 320 - 280 = 40px に抑える必要があります。枠線 4px(左右計 8px)を変更しない場合、左右paddingの合計は 40 - 8 = 32px となり、片側 16px に設定します。

/* 改善案の提示(※提案コード。未実行) */
.box-adjusted {
  box-sizing: border-box;
  width: 320px;
  padding: 16px;
  border: 4px solid #177887;
  margin: 16px 0;
}

この指定により、全体の幅は 320px のままコンテンツ幅が 320 - (16 × 2) - (4 × 2) = 280px となります。

5. はみ出すカードから固定幅を見つける

画面の幅が狭くなったとき、配置したカード要素が枠線を突き抜けて右側へ飛び出してしまう現象に遭遇することがあります。本章では、狭い枠組みの中で意図せずはみ出してしまうカード要素を起点として、どの指定が寸法を縛っているのかを特定し、流動的な幅へ安全に整える調査手順を解説します。

突き抜けた要素とスクロールの発生

内容の寸法が親の領域を超えたとき、はみ出した部分がどう見えるかはoverflowなどの指定によって変わります。この教材では親が表示領域を制限しているため、文章の一部が境界で切れる様子を調べます。

観察には、HTTPサーバーを介してHTMLとJSONを配信する検証環境(http://127.0.0.1:9033/demo/)を用います。通常のデスクトップ版Chromeの表示領域内に配置した360pxの固定枠の中で何が起きているのかを追います。

<!-- demo/index.html の抜粋 -->
<div class="frame">
  <div id="product" class="fixed">
    <div class="product">
      <div class="art" aria-hidden="true">☕</div>
      <div>
        <h3>朝のコーヒー</h3>
        <p>長い説明文でも内容を読めるカードを作ります。</p>
      </div>
    </div>
  </div>
</div>
/* demo/index.html の抜粋 */
.frame{width:360px;max-width:100%;border:2px dashed #9b6848;padding:12px;overflow:auto}
.fixed{width:520px;background:#e3eff3;padding:20px}

この枠組みでは、茶色い破線の境界を持つ親要素.frameの内側に、商品情報を提示するカード要素#productが配置されています。初期状態の画面を表示させた様子が次の図です。

幅520pxのカードが教材のコンテナからはみ出す

薄い青色のカードは親の内容幅を超え、右側の文章が表示領域の境界で切れています。この画像には水平スクロールバーは明瞭に写っていません。コード上は横方向にスクロールできる領域ですが、スクロール操作そのものは今回確認していません。枠の境界線とカードの終端が食い違っており、右端に追いやられた文字列はスクロールを動かさなければ読めない状態です。

親要素の内容幅とカードの固定幅を計算する

はみ出しが発生している理由を説明するために、親要素がカードを受け入れられる有効な寸法(内容幅)を算出します。

教材のスタイルシートでは、全要素に対してbox-sizing: border-boxが適用されています。したがって、.frameに指定されたwidth: 360pxという値は、左右の枠線とパディングを含んだ外側の合計値となります。CSSの記述から数値を拾い出すと、左右のボーダー幅はそれぞれ2pxであり、左右のパディングはそれぞれ12pxです。ここから親要素が内側の要素に割り当てられる内容幅(content boxの幅)を逆算します。

親要素の全体幅360pxから左右のボーダー幅(2px × 2 = 4px)を引き、さらに左右のパディング幅(12px × 2 = 24px)を差し引きます。この減算により、360px - 4px - 24px = 332pxという数値が導かれます。つまり、親要素が子要素を収納できる横幅は実質的に332pxです。

それに対して、子要素である#productには.fixedクラスが付与されており、width: 520pxという固定値が宣言されています。親が差し出せる332pxの空間に対して、子が520pxの占有を要求しているため、差分である188px分だけ親の内容領域から溢れ出します。この直接の破綻を踏まえ、Stylesペインから幅を絶対値で固定している宣言がないかを確かめる作業が初動となります。

overflowプロパティが作るスクロール領域の役割

親要素の定義に含まれているoverflow: autoは、内容が枠をはみ出した際にブラウザーが自動的にスクロールバーを生成し、境界外の領域を閲覧可能にする指定です。

スクロールバーの出現により文字や画像が画面外へ完全に消失する事態は免れていますが、これは親要素がはみ出しを局所的に封じ込めた結果にすぎず、カード自体が不適切な寸法を保ったまま押し込まれている事実に変わりはありません。固定幅のままでスクロール領域に依存すると、読み手は横方向へのスクロール操作を強いられます。overflow: autoは過剰な幅を要求している原因そのものを解消する指定ではないため、スクロールバーの存在を手がかりとして受け止めつつ、過剰な幅を持つ子要素の特定へ進める必要があります。

固定幅から百分率と上限値への移行

原因がwidth: 520pxという固定値にあることが判明したため、次はカードの寸法指定を親要素の幅に適応させる修正を検討します。教材画面の「親の幅に収める」ボタンを押すと、カードに適用されるクラスが.fixedから.responsiveへと切り替わります。

/* demo/index.html の抜粋 */
.responsive{width:100%;max-width:520px;background:#e3eff3;padding:20px}

このCSS定義では、基本の幅としてwidth: 100%を指定しつつ、追加でmax-width: 520pxという上限値を設けています。親要素の内容幅が520pxを下回る狭い領域ではwidth: 100%が働き、カードの幅は親の内容幅である332pxに一致するよう縮小されます。逆に、親要素が十分に広い大画面に置かれた場合はmax-width: 520pxが歯止めとなり、カードが必要以上に横に間延びすることを防ぎます。

切り替えボタンを押した直後の教材画面の様子が次の図です。

幅を100%へ変更したカード内で文章が折り返す

カードの右端が親要素の右内枠(パディングの手前)で揃い、外枠の破線を越えなくなっています。カード内の説明文「長い説明文でも内容を読めるカードを作ります。」がカードの横幅に合わせて適切に折り返されています。

要素の寸法と表示状態の比較

二つの状態について、ソースから計算した寸法と画面上の挙動を整理します。カードの実効幅はコードからの計算値であり、独立した測定ログを採った値ではありません。

観測対象 固定幅の初期状態(.fixed) 改善後の流動状態(.responsive)
親枠(.frame)の全体幅 360px(border-box) 360px(border-box)
親枠の左右ボーダー合計 4px(左2px + 右2px) 4px(左2px + 右2px)
親枠の左右パディング合計 24px(左12px + 右12px) 24px(左12px + 右12px)
親枠の有効内容幅 332px 332px
カード(#product)の指定幅 width: 520px width: 100%; max-width: 520px
カードの実効幅 520px 332px
親の内容幅との差分 +188px(過剰) 0px(過不足なし)
横方向の状態 右側の内容が表示領域で切れる 掲載文が枠内に収まる
テキストの改行動作 右端で延長し一部が不可視領域へ進入 枠内で自動的に折り返し完了

外枠コンテナの寸法や余白設定は一切変更していません。内側に配置されたカードが親の内容幅332pxを尊重する指定に改められただけで、レイアウト全体の整合性が回復しています。

内部レイアウトの破綻を防ぐ追加調査

カード全体の幅を整えたあとも、カード内部に配置されたFlexboxコンテナや予期せぬ文字列の流入が引き起こす問題について切り分けの視点を持っておく必要があります。

Elementsパネルでは、要素がFlexboxレイアウトを形成している場合にDOMツリー上で専用のバッジが表示されます。Chrome DevToolsのFlexbox解説にあるとおり、Elementsパネルのflexバッジをクリックするとオーバーレイが描画され、アイテムの並び順や余白の配置状況を検査できます。

カードの内部構造を確認すると、アイコン表示領域.artとテキスト群を包む領域が、親の.productによって横並びに配置されています。

<!-- demo/index.html の抜粋 -->
<div class="product">
  <div class="art" aria-hidden="true">☕</div>
  <div>
    <h3>朝のコーヒー</h3>
    <p>長い説明文でも内容を読めるカードを作ります。</p>
  </div>
</div>
/* demo/index.html の抜粋 */
.product{display:flex;gap:18px;align-items:center}
.art{width:160px;aspect-ratio:4/3;background:linear-gradient(140deg,#d9b467,#eddbaf);border-radius:7px;display:grid;place-items:center;font-size:56px}

この横並び配置において、説明文の中に極端に長い英単語や途切れのないURL記号列が紛れ込んだ場合、新たなはみ出しが発生する懸念があります。Flexアイテムの横幅の最小値を示すmin-widthプロパティは初期値がautoであり、折り返せない長い文字列が存在すると文字列の長さを優先してアイテムを押し広げてしまいます。

左側の.artには160pxが指定されていますが、flexアイテムとして縮む場合があります。gapの18pxも含め、絵と文章が限られた幅を分け合います。このようなはみ出しを防ぐ机上の追加対策例として、以下の2行が考えられます(教材内では未実行の提案コードです)。

/* 机上の追加対策例(教材内では未実行の提案コード) */
.product > div:last-child {
  min-width: 0;
  overflow-wrap: anywhere;
}

min-width: 0はFlexアイテムが親の許容幅まで縮小(flex収縮)できるように制限を解除する指定です。overflow-wrap: anywhereは改行位置のない連続文字列であっても枠線の手前で強制的に改行を発生させる指定です。この案は長い文字列によるはみ出しを減らすための候補です。内容と幅を変えた追試で効果を確かめる必要があります。

6. 商品データの取得を画面と結び付ける

Webアプリケーションの不具合調査では、画面に文字が現れない原因が通信自体の失敗なのか、受信後の処理にあるのかを取り違えやすくなります。ここではローカルの検証用ページを用いて、ボタン操作から画面に商品データが反映されるまでの過程を分解し、観測の基準を整理します。

画面の変化と三つの成否基準

ユーザーが「商品データを取得」ボタンを押してから画面にデータが表示されるまでの処理は、大きく三つの段階に整理できます。第一がサーバーとHTTP通信を行って応答を受け取る応答成功、第二が受け取ったテキストをオブジェクトへ変換するparse成功、第三が取り出した値をもとにDOM要素を書き換える描画成功です。

画面に文字が出ない現象に対して、通信が失敗していると即断してはいけません。通信自体はHTTPステータス200で成功していても、その後のJSON解析で構文エラーが起きれば描画には届きません。逆にJSON解析が進んでも、反映先要素の指定が誤っていれば画面には表示されません。まずは通信経路の境界線で起きた事象を確かめ、次にプログラム内部の解釈を確かめる順序で切り分けます。

教材の通信コードとデータ構造

本記事の教材は、HTML(demo/index.html)とJSON(product.json)の2ファイルをHTTPサーバーで配信して動作させます。この教材の成功例を再現するには、HTMLとは別にproduct.jsonを配信する必要があります。観察対象となるスクリプトの処理構造を確認します。

// demo/index.htmlの抜粋
if(mode==='network'){
  async function load(path){
    const out=document.querySelector('#networkResult');
    out.textContent='取得中です。';
    out.className='result';
    try{
      const response=await fetch(path,{cache:'no-store'});
      if(!response.ok){
        out.textContent=`HTTP ${response.status}:データを取得できませんでした。URLを確認してください。`;
        out.classList.add('bad');
        return
      }
      const data=await response.json();
      out.textContent=`HTTP ${response.status}:${data.name} / 在庫 ${data.stock} 個`
    }catch{
      out.textContent='応答を受け取れませんでした。接続状態を確認してください。';
      out.classList.add('bad')
    }
  }
  document.querySelector('#success').onclick=()=>load('product.json');
  document.querySelector('#failure').onclick=()=>load('missing-product.json')
}

ボタンをクリックするとDOMの文言が「取得中です。」へ更新され、その後に fetch() による非同期通信が開始されます。リクエスト対象の product.json は次の形式です。

{"name":"朝のコーヒー","stock":12}

スクリプトは await response.json() で文字列を解析し、得られたオブジェクトから data.name と data.stock の値を取り出します。最後に #networkResult 要素の textContent へ、ステータス番号と結合した文字列を代入して描画を完了します。

教材ページにおける正常表示の観測

HTTPサーバーを起動し、macOS上のChromeでローカル教材ページ(http://127.0.0.1:9033/demo/?case=network)を開いて「商品データを取得」ボタンを押したときの画面出力を確認します。次の画像は、教材画面で取得処理が完了した状態を記録したものです。

商品データを取得しHTTP200と在庫12個を表示

ボタン下部の領域に「HTTP 200:朝のコーヒー / 在庫 12 個」という緑枠のメッセージが表示されます。この表示から、通信が応答を返し、スクリプトがJSONを解釈して枠内に出力した結果が読み取れます。ただし、この画面表示だけでは通信の中身そのものを直接観察したわけではないという限界があります。通信の一次情報に直接触れて裏付けを取る必要があります。

Networkパネルを使った追試手順

画面上の結果を客観的事実と照合するため、Chrome DevToolsのNetworkパネルを利用します。公式の解説であるChrome DevTools Networkガイドでも、通信を記録して個別の要求のヘッダーや応答内容を確かめる基本手順が示されています。

追試は次の順序で進めます。

  1. 教材ページを開いた状態でDevToolsを立ち上げ、「Network」タブを選択します。
  2. 左上の記録ボタンが赤色になり、通信の記録状態になっていることを確認します。
  3. 必要に応じて「Clear」ボタンを押し、既存のログを消去します。
  4. 画面上の「商品データを取得」ボタンをクリックします。
  5. Networkパネルのリクエスト一覧に現れた product.json をクリックします。
  6. 右側の詳細画面で「Headers」タブと「Response」タブを開き、結果を検査します。

「Headers」ではリクエストURLやステータスコードが 200 OK であるかを確認できます。「Response」タブではサーバーから返された応答本文のテキストを確認でき、{"name":"朝のコーヒー","stock":12} が届いているかを検証できます。

押した瞬間と表示時点の時間差を追う

非同期通信では、ボタンがクリックされてからサーバーが応答を返し、ブラウザーが処理するまでに物理的な時間がかかります。教材コードでも load() の先頭で out.textContent = '取得中です。' を実行しています。コード上ではこの中間の文字列をDOMへ代入します。ただし、通信が速ければブラウザーが画面へ描く前に最終表示へ変わることがあり、人の目で必ず見えるとは限りません。今回、取得中の状態は撮影していません。

ボタンを押しても画面が変わらない場合、クリックイベントの未発火と即断せず、Networkパネルで要求が送信中(Pending)か完了済みかを確認します。時間軸に沿って状態の遷移を追うことが重要です。

三つの段階を切り分ける調査手順

画面に意図したデータが出ないときは、境界線を意識して次の表に沿って調査を進めます。

調査の着眼点 観察する場所 期待される正常な状態 この段階が失敗している場合の例
応答成功の確認 DevToolsのNetworkパネル(Headers) ステータスコードが200番台で返る 404 Not Found、500エラー、通信の切断
parse成功の確認 NetworkパネルのResponseとConsole 有効なJSONがオブジェクト化される JSONの文法誤りによるSyntaxErrorの発生
描画成功の確認 Elementsパネルおよび画面上の表示 取り出した値が目的のDOM要素に入る 代入先要素が存在しない、プロパティ名の指定誤り

最初の切り分けでは表の上から順に見る方法が使えます。大元の応答が404エラーであったりJSONが破損していたりする場合、描画処理側のコードを修正しても解決しないためです。

なお、表示崩れや古いスクリプトの残存が疑われる場合でも、任意のCSSやJSを再読み込みすれば必ず復旧するわけではありません。本教材においては、ボタン操作によって再度通信と描画の関数を実行することで状態の変化を再検証できます。

発展的な確認のための演習

挙動の変化を観察するために、次の机上の検証コードを例として挙げます(実際の動作記録ではありません)。

// 机上の検証用コード(未実行の例)
// 応答成功とparse成功の境目を観察するために、構文の崩れた文字列を扱う例
async function testParseBoundary() {
  const dummyResponse = new Response('{"name": "壊れたデータ", stock: }');
  try {
    const data = await dummyResponse.json();
    console.log('parse成功:', data);
  } catch (error) {
    console.error('parse失敗を検知:', error);
  }
}

この例では、レスポンスの生成自体が成立していても、dummyResponse.json() の呼び出し時に構文解析エラーが発生して catch ブロックへ分岐します。

また、教材の product.json のキーを {"title": "朝のコーヒー", "quantity": 12} に変更すると、通信とJSONパースは成功しますが、スクリプトが data.name を参照するため画面には undefined が表示されます。表示の不具合がすべてHTMLとJavaScriptの差異のみから生じるわけではなく、データ構造の不一致など多様な原因が存在します。

7. 404と接続失敗を混同しない

画面の赤色と通信の成否を分ける

ウェブアプリケーションで赤い警告枠が表示されても、通信自体が切断されたりサーバーが停止したりしたとは限りません。画面の警告表示と、ネットワーク層で起きた現象は区別する必要があります。Chrome DevToolsのNetworkパネルの概要でも示されているように、通信の事実を確かめる作業が欠かせません。

教材(http://127.0.0.1:9033/demo/)の通信パネルにある「存在しないデータを取得」ボタンを押すと、画面上には薄赤い背景でメッセージが現れます。この挙動をローカル環境のmacOS版Chromeで操作して記録した様子が次の画面です。

存在しないデータへのHTTP404を案内する画面

画面には「HTTP 404:データを取得できませんでした。URLを確認してください。」と表示されます。この結果は通信が途絶したわけでも、JavaScriptの非同期処理が例外を投げて異常終了したわけでもありません。要求はローカルサーバーへ正常に届き、サーバーから404というステータスコードを伴ったHTTP応答が返ってきた結果として表示されています。

fetchにおけるrejectとHTTPエラー応答の差

JavaScriptのfetch関数を扱う際、HTTPステータスが400番台や500番台であればPromiseが自動的にrejectされると誤認されることがあります。

fetchが返すPromiseがrejectされる条件は、端末のオフライン、名前解決の失敗、接続タイムアウトなど、通信上の失敗に加え、不正な要求や中断などでも起こります。サーバーと接続し、相手からHTTP応答ヘッダーを受け取った時点で、その応答が404 Not Foundや500 Internal Server Errorであっても、Promiseはresolveされます。HTTPのエラーステータスとPromiseのrejectは別の分類です。Fetch Standardでも応答処理と中断などの失敗処理が区別されています。

教材スクリプトdemo/index.htmlの該当箇所を確認します。なお、本教材はHTMLとJSONの二ファイルをHTTPサーバーで配信する構成をとっています。

通信の章で掲載したload関数では、fetchの直後にresponse.okを調べ、偽なら表示を更新してreturnしています。以下ではその分岐を追います。

「存在しないデータを取得」ボタンを押すと引数pathにmissing-product.jsonが渡されます。サーバー上に配置されていないため404が返りますが、実行の流れは直ちにcatchブロックへ移りません。response変数にResponseオブジェクトが格納され、if(!response.ok)が評価されます。

response.okによる明示的な遮断と早期return

response.okプロパティは、HTTPステータスコードが200から299までの範囲にある場合にtrueとなり、それ以外ではfalseを返します。教材コードでは404応答を受け取った際にfalseとなるため、if文の本体が実行され、直後のreturnで処理を中断します。

もし早期リターンを置かずに続行させると、後続のresponse.json()が呼び出されます。Webサーバーが返す404応答の本文はHTMLエラーページなどの場合があり、その状態でresponse.json()を呼ぶと構文エラー(SyntaxError)が送出されます。

サーバーの404応答(HTML本文)
  ↓
response.ok の検査を怠る
  ↓
response.json() を実行する
  ↓
SyntaxError が発生して catch ブロックへ移る
  ↓
画面に「通信失敗」と誤認表示される

404を受け取った際にメッセージを設定して直ちにreturnしているのは、この構文解析エラーによる誤遷移を防ぐ設計です。

一つのcatchに集約される教材コードの限界

教材のコードでは、例外の受け皿が単一のcatchブロックしかありません。この記述により、物理的な回線切断などによる「接続失敗」と、HTTP 200応答の本文破損による「JSON解析失敗」が区別されず、同じメッセージで扱われます。

失敗の種類 発生箇所 通信の成否 教材コードでの画面表示
HTTP 404 サーバー応答 成功(404受信) 「HTTP 404:データを取得できませんでした…」
接続失敗 fetchの呼び出し 失敗(到達不能) 「応答を受け取れませんでした…」
JSON解析失敗 response.json() 成功(200受信) 「応答を受け取れませんでした…」

通信自体は成功していても、JSON解析に失敗すると「応答を受け取れませんでした。接続状態を確認してください。」と表示されます。二つの原因を分離して扱う設計案を以下に示します。

// 原因を分離して画面に反映する実装案(※机上の例・未実行)
async function loadDetailed(path) {
  const out = document.querySelector('#networkResult');
  out.textContent = '取得中です。';
  out.className = 'result';

  let response;
  try {
    response = await fetch(path, { cache: 'no-store' });
  } catch (networkError) {
    out.textContent = '要求を完了できませんでした。接続状態や中断の有無を確認してください。';
    out.classList.add('bad');
    return;
  }

  if (!response.ok) {
    out.textContent = `HTTP ${response.status}:要求を処理できませんでした。`;
    out.classList.add('bad');
    return;
  }

  try {
    const data = await response.json();
    out.textContent = `HTTP ${response.status}:${data.name} / 在庫 ${data.stock} 個`;
  } catch (parseError) {
    out.textContent = '応答本文の読み取りまたは解析に失敗しました。';
    out.classList.add('bad');
  }
}

観測事実と推測の境界を意識する

本稿の検証は、macOS上のChromeを用い、同一ホスト上で稼働するローカル教材ページを操作して得られた観測に基づいています。missing-product.jsonへの要求に対して404ステータスが返り、画面の条件分岐を経てメッセージが表示されたことまでは記録された事実です。

一方、オリジン間リソース共有(CORS)違反による遮断、ブラウザーがオフラインの際の挙動、ネットワーク速度制限、実機モバイル端末や他社製ブラウザー間の挙動差については検証を行っていません。また、本教材の画面表示が崩れた場合の復旧についても、今回記録したボタン操作と表示結果に限られ、任意の環境での復旧を保証するものではありません。

404調査の手順を整理する

リソースが見つからない場合にとるべき論理的な作業手順は以下の通りです。

  • 要求URLの完全な一致を確認する:大文字・小文字、拡張子、相対パスの基準位置のずれがないか点検します。
  • HTTPステータスコードを直接確認する:JavaScriptのcatchに頼らず、通信記録からサーバーが返したエラーコードが404であることを確認します。
  • 応答ヘッダーと本文の形式を確認する:サーバーのContent-Typeヘッダーを確認し、JSONを期待する処理にHTML等が返っていないかを確かめます。
  • フロントエンド側の分岐論理を照合する:response.okの判定が行われているか、404受信後に本文の解析処理へそのまま突入していないかを確認します。

8. フォームのエラーを入力欄と結び付ける

入力欄で不備が生じた際、画面のどこに問題があるのかを利用者が即座に把握し、迷わず修正へ移れる設計は欠かせません。 本章では、ローカル教材のフォーム画面を用いて、確認ボタンを押したときに起きる現象、属性による要素同士の結び付き、そして入力値の検証状態を調査する手順を整理します。

空欄時の検証とフォーカス移動の観察

教材画面のナビゲーションから「フォーム」を選ぶと、メールアドレスの入力欄と入力確認用のボタンが配置された領域が表示されます。 まず入力欄に何も文字を入れない空欄の状態で、「入力を確認」ボタンを押したときの挙動を確認します。 画面上には赤色を基調とした枠で「メールアドレスを入力してください。」という警告文が現れ、同時にカーソルが自動的に入力欄の中へ移動します。

空欄を確認した後の入力案内とフォーカス

入力欄の内部に薄い灰色で表示されている案内文はプレースホルダーと呼ばれ、入力形式の見本を示すものであり入力値そのものとしては扱われません。 未入力のまま送信を試みた際に、警告文の提示だけでなく入力欄へフォーカスを自動で戻す挙動は、利用者が次にどの場所へ向かうべきかを直感的に伝える役割を果たします。

教材のHTMLと検証スクリプトの構造

この動作がどのようなマークアップとスクリプトによって成立しているのか、教材のソースコードから読み解きます。 以下は demo/index.html の抜粋です。

<form id="practiceForm" novalidate>
  <label for="email">連絡先メールアドレス(教材用)</label>
  <input id="email" type="email" required aria-describedby="emailHelp formMessage" placeholder="reader@example.test">
  <p id="emailHelp">架空のアドレスで練習できます。入力は外部に送信しません。</p>
  <button type="submit">入力を確認</button>
  <p id="formMessage" class="result" role="status">未確認です。</p>
</form>

送信イベント発生時の処理は、次のように記述されています。こちらも demo/index.html の抜粋です。

if(mode==='form'){
  document.querySelector('#practiceForm').onsubmit=(event)=>{
    event.preventDefault();
    const email=document.querySelector('#email');
    const out=document.querySelector('#formMessage');
    const valid=email.validity.valid;
    email.setAttribute('aria-invalid',String(!valid));
    out.className=valid?'result':'result bad';
    out.textContent=valid?'入力形式を確認しました。サーバーへの送信はしていません。':email.validity.valueMissing?'メールアドレスを入力してください。':'メールアドレスの形式を確認してください。';
    if(!valid)email.focus()
  }
}

この実装において、form タグには novalidate 属性が指定されています。 この属性を付与するとブラウザー固有のポップアップ通知は抑止されますが、ブラウザーが備える制約の判定機能まで止まるわけではありません。 WHATWG HTML Standardの制約検証APIの仕様にある通り、各入力要素の validity プロパティを参照することで、入力必須項目が空であるかを示す valueMissing などの真偽値をスクリプト側から判定できます。

支援技術へ状態を伝える三つの属性

調査において重要なのは、視覚的な変化と同時に、支援技術へ向けた属性がどのように構成されているかを確かめることです。 教材のHTML構造では、要素同士を結び付けるために次の三つの属性が使われています。

  • ラベルの明示:label 要素の for 属性と input 要素の id 属性の値を一致させることで、入力欄とラベルを関連付けます。
  • 補足説明の関連付け:aria-describedby 属性に半角スペース区切りで二つのID(emailHelp formMessage)を指定し、事前の注記と動的に変化する結果表示領域の両方を入力欄へ結び付けます。
  • 不備状態の通知:検証に失敗したタイミングでスクリプトが aria-invalid="true" を設定し、入力欄が不備を抱えている事実を伝達します。

有効な形式の入力と通信の非発生

続いて、入力欄へ教材で例示されている reader@example.test という文字列を入力し、再度「入力を確認」ボタンを押します。 すると赤い警告枠は消去され、緑色の枠の中に「入力形式を確認しました。サーバーへの送信はしていません。」という案内文が表示されます。

メール形式の確認後。サーバーへは送信していない

ここで確認できた事実は、入力された文字列がブラウザーの構文規則を満たしていると判定されたことだけです。 教材は送信イベントの既定動作をpreventDefault()で止め、そのハンドラーにも通信処理を記述していません。preventDefault()だけで任意のJavaScript通信まで禁止されるわけではありません。 画面上の検証は書式上の明らかな間違いを即座に利用者に気付かせるための仕組みであり、実在性や到達性の確認ではないという境界を意識する必要があります。

入力値検証の挙動切り分け表

空欄とreader@example.testは実際の画面で確認しました。次の表のreaderとreader@はソースと入力制約からの予想であり、今回の操作記録には含まれません。追試候補として区別してください。

入力値の条件 参照される判定プロパティ aria-invalid の値 画面の表示文言 フォーカス移動
空欄(未入力) validity.valueMissing === true "true" メールアドレスを入力してください。 入力欄へ自動移動
reader(@記号なし) validity.typeMismatch === true "true" メールアドレスの形式を確認してください。 入力欄へ自動移動
reader@(ドメイン欠落) validity.typeMismatch === true "true" メールアドレスの形式を確認してください。 入力欄へ自動移動
reader@example.test validity.valid === true "false" 入力形式を確認しました。サーバーへの送信はしていません。 フォーカス移動なし

値が存在しないことによる違反と、構文が誤っている違反とでは、スクリプトが参照している内部フラグが分かれています。

境界値の追加検証案

実際の運用で生じる多様な入力に対応するためには、さらに踏み込んだ境界値の振る舞いを点検する必要があります。 追加で検証すべき値として次の四つの観点を提案します。

第一に、前後に半角空白が含まれた文字列(例:" reader@example.test ")の入力です。貼り付けた文字列とinput.valueとして取得できる値を比較し、ブラウザーの値の正規化を含めて確かめる課題です。 第二に、全角英数字や全角記号(例:reader@example.test)の入力です。自動的に半角へ正規化するのか、再入力を求めるのかを判断する材料になります。 第三に、実在性が疑わしい特殊なドメイン形式(例:reader@localhost やドメイン部に記号のみが並ぶ形式)の入力です。フロントエンドの段階で厳密に弾くべきかどうかを検討する手掛かりとなります。 第四に、表示幅を大きく超える長い文字列の入力です。表示領域から文字があふれてレイアウトを損ねないかを確かめる上で有効です。

調査手順の整理と改善コードの考察

フォームのエラー処理を調査する手順は次の通りです。 まず、送信操作を試みた直後にフォーカスが目的の入力欄へ移動したかを確かめます。 次に、入力欄要素に対して aria-invalid 属性が適切に付与または解除されているかを点検します。 最後に、表示された警告メッセージのIDが aria-describedby 属性に含まれており、入力欄の補足情報として認識される状態になっているかを確かめます。

教材の実装では、不備が見つかった際に対象となる要素を直接指定して email.focus() を呼び出しています。 複数項目の検証に対応させるための机上の改善提案コードを以下に示します。なお、本コードは設計の考え方を説明するための提案であり、本教材の環境では未実行のコードです。

// 未実行の改善提案コード
const invalidField = form.querySelector(':invalid');
if (invalidField) {
  invalidField.focus();
}

:invalid 擬似クラスを問い合わせることで、DOMツリーの中で最初に検証に引っかかった入力欄を特定し、利用者の操作位置を先頭のエラー箇所へと導くことができます。

9. キーボードで操作の位置を追う

操作の現在地を画面上で見失う現象

マウス操作ではカーソルを重ねて要素をクリックしますが、キーボード操作ではフォーカスを示す視覚的な手がかりに頼って操作位置を把握します。Tabキーを押しても枠線が表示されなかったり、操作の前後で表示位置が飛んだりすると現在地を見失います。操作の現在地を正確に追うためには、キーボード入力を通じて画面上で生じる輪郭線の変化と要素の状態を切り離して観察する必要があります。本章ではローカル教材の画面を用い、Tabキーによる移動、輪郭線の可視性、要素の状態変化を観測します。

教材のキーボード操作画面とフォーカス表示の確認

教材画面の構造を確認します。対象は http://127.0.0.1:9033/demo/?case=focus で呼び出される練習パネルです。教材はHTMLとJSONの2ファイルをHTTPサーバーで配信して動作します。

以下は、demo/index.html の抜粋です。

button:focus-visible,input:focus-visible,a:focus-visible{outline:4px solid #cc7300;outline-offset:4px}

この教材では、:focus-visible 疑似クラスに対して太さ 4px のオレンジ色(#cc7300)の実線輪郭と、要素の外側へ 4px 離す outline-offset が定義されています。HTML内には「練習操作へ移動」というアンカーリンク、そして「最初の操作」ボタンと「次の操作」ボタンが配置されています。

次の画像は、最初のボタンをクリックしたあとにTabキーを一度押した際の実観測結果です。

Tabで次の操作へ移動したフォーカス。直前の操作結果は残る

オレンジ色の輪郭線が「次の操作」ボタンの周囲に表示される一方、ステータスメッセージ欄には「最初の操作を実行しました。」という文字列が残っています。

観測結果の切り分けとフォーカスと実行の分離

前掲の画像の状態を読み解くには、「現在どの要素が入力の標的になっているか」というフォーカス(focus)と、「その要素の動作が呼び出されたか」というアクティベーション(activation、実行)を分離して評価します。

実観測の手順では、まず「最初の操作」ボタンをクリックしました。これにより結果通知欄に「最初の操作を実行しました。」と書き込まれました。その直後にTabキーを押下し、フォーカスが次の「次の操作」ボタンへ前進しました。

Tabキーでフォーカスを「次の操作」へ移動させただけでは、そのボタンのクリック処理は実行されません。結果通知欄の文言が変化していない事実は、フォーカスの移動とアクションの実行が別々の事象である証拠です。公式ガイドの Track element focus でも、現在フォーカスを持つ要素を特定する手法として document.activeElement の参照が示されています。この教材では、現在のフォーカス位置と実行済みの操作結果を別々に確認します。

フォーカス移動と操作実行の整理

押下したキーが「フォーカスの移動」を担うのか、「要素の実行」を担うのかを分類します。以下の表は標準的な要素で期待する挙動を整理した追試用の表です。各キーを今回すべて試した結果ではありません。

キー操作 挙動の種別 リンク(<a>)での動作 ボタン(<button>)での動作
Tab フォーカス移動 次のフォーカス可能要素へ進む 次のフォーカス可能要素へ進む
Shift + Tab フォーカス移動 前のフォーカス可能要素へ戻る 前のフォーカス可能要素へ戻る
Enter 操作の実行(activation) リンク先へ遷移(教材ではページ内移動) ボタンのクリック処理を実行する
Space 操作の実行(activation) 通常はページスクロール(リンクは未実行) ボタンのクリック処理を実行する

TabキーおよびShift+Tabキーの役割はフォーカス位置の変更にとどまります。一方、実行キーの挙動はHTML要素によって異なります。ボタン要素はEnterキーとSpaceキーの双方がクリックイベントを誘発しますが、標準的なリンク要素ではEnterキーのみが遷移を実行し、Spaceキーはスクロールとして処理されます。

巡回順序と逆方向移動の追試手順

教材を対象にした具体的な調査手順を組み立てます。

まずアドレスバーやページ最上部から始め、マウスを使わずにTabキーを単独で押下していきます。ナビゲーションリンク群を通過し、メインコンテンツ内の「練習操作へ移動」リンク、「最初の操作」ボタン、「次の操作」ボタンへとフォーカスが順に移動することを目視で確認します。HTMLソースのDOMの並び順とフォーカス移動順が一致しているかを照合します。CSSプロパティで視覚的な並び順だけを入れ替えている場合、フォーカス移動が画面上で不規則に跳躍する現象が発生します。

次に逆方向の移動を確認します。「次の操作」ボタンに輪郭線がある状態でShift+Tabキーを押します。フォーカスが直前の「最初の操作」ボタンへ巻き戻り、後退経路が確保されているかを確認します。

リンクとボタンの実行追試案

各要素の実行(activation)に関する追試を行います。「最初の操作」にフォーカスがある状態でSpaceキーを押し、結果通知欄が「最初の操作を実行しました。」に更新されるかを確認します。続いてTabキーで「次の操作」へ移り、Enterキーを押して結果通知欄が「次の操作を実行しました。」へ書き換わるかを確認します。

次にアンカーリンクを検証します。Shift+Tabキーで「練習操作へ移動」リンクへ戻し、SpaceキーとEnterキーの挙動を比較します。標準ブラウザ仕様ではSpaceキーでリンク遷移は発生せず、Enterキーでターゲット要素 #practice へのジャンプが発生します。

スクリプトによる動的フォーカス制御の検証案

JavaScriptからプログラム的にフォーカスを移動させる場合の変更案を検討します。以下は机上の提案コードであり、本章の執筆時点では未実行の例です。実観測の記録とは区別して扱います。

// 提案コード(机上の例・未実行):操作完了後にフォーカスを結果領域へ移す試み
const practiceSection = document.getElementById('practice');
const resultArea = document.getElementById('focusResult');

practiceSection.addEventListener('click', (event) => {
  if (event.target.tagName === 'BUTTON') {
    resultArea.setAttribute('tabindex', '-1');
    resultArea.focus();
  }
});

この提案コードでは、ボタンがクリックされた直後に結果表示領域(<p id="focusResult">)へフォーカスを移動させます。非対話的要素に tabindex="-1" を付与することで、スクリプトからの .focus() 呼び出しが可能になります。この教材のボタン操作においてメッセージへ注意を向ける手段になりますが、キーボードで連続操作を行う際にフォーカスを戻す手間が生じる点にも配慮が必要です。

今回の調査範囲

本章の実観測は、macOS上のChrome環境において教材ページの画面表示とキーボード入力を突き合わせた範囲に限られます。実施したのは「最初の操作」のクリック後、Tabを一度押して「次の操作」へ移動し、輪郭線とメッセージを確認する操作です。Shift+Tab、Enter、Spaceおよびリンクへのキー操作は追試案であり、今回の記録には含みません。

スクリーンリーダーによる音声読み上げ、Windows環境や他社製ブラウザでの検証、モバイル実機検証、パフォーマンストレースなどは本章の作業範囲には含まれていません。画面上に輪郭線が見えていることと、支援技術へ名前や役割が正しく伝達されているかは別の検証事項となります。

10. 見えるラベルとアクセシビリティ情報を照合する

画面の文字と支援技術へ渡る情報の食い違い

画面上で見えている文字列と支援技術に伝達される情報は必ずしも一致しません。ボタンや入力欄に直感的な文字が表示されていても、支援技術が参照する内部情報が欠落していたり、異なる内容が設定されていたりすることがあります。

ブラウザーはHTMLを解析してDOMツリーを構築すると同時に、支援技術にUI情報を中継するアクセシビリティツリーを生成します。ここでは画面上の要素ごとに名前(Name)、役割(Role)、状態(State)が整理されます。

Chrome DevToolsのElementsパネルにあるAccessibilityペインでは、算出された属性を確認できます。公式解説のChrome DevTools Accessibility Referenceにある通り、ツリーやプロパティの調査が可能です。画面表示と算出値を突き合わせることで、ラベルの上書きや状態管理の不備を切り分けられます。

入力欄と開閉操作から組み立てる調査設計

この章は教材ソースを材料にした追試の設計です。Accessibilityペインのプロパティ表示や実際の音声読み上げを今回確認した記録ではありません。

今回の照合演習では、教材サーバーを起動し、ブラウザーで http://127.0.0.1:9033/demo/?case=form や http://127.0.0.1:9033/demo/?case=dom を開いて対象画面を確認します。教材はHTMLとJSONの2ファイルを配信する構成です。

まず、demo/index.html の入力フォーム周辺のマークアップです。

フォームの章で掲載したform、label、inputと結果表示を使って追試します。

調査では、input の名前が <label> のテキストから計算されているか、placeholder が誤って名前に採用されていないか、#emailHelp が aria-describedby 経由で算出されているかを確認します。未入力送信時にスクリプトで aria-invalid が付与されるかも対象とします。

トグルボタンの初期実装は次の通りです。

<!-- demo/index.html の抜粋 -->
<button id="toggle" aria-expanded="false" aria-controls="detail">詳細を表示</button>
<div id="detail"></div>

役割が「button」、名前が「詳細を表示」となっているかを確認します。クリック直後に aria-expanded が「false」から「true」へ反転し、ボタン自身のテキストが「詳細を閉じる」に切り替わるかという変化を追跡します。

名前とプレースホルダーの計算順序を切り分ける

テキストボックス内の薄い文字列は形式の例を示す補助情報であり、入力欄の永続的な名前ではありません。

教材のフォームでは、<label for="email"> により「連絡先メールアドレス(教材用)」が提供され、input には placeholder="reader@example.test" が指定されています。Accessibilityペインでこの要素を選択すると、算出された名前(Computed Name)の算出元として label 要素が優先され、placeholder は補助的な位置づけであることが確認できます。

<label> との紐付けが欠落している場合、ブラウザーはフォールバックとして placeholder を名前に採用することがあります。照合作業では名前の値だけでなく、どの要素や属性から導き出されたのかという計算経路まで確認します。

あわせて aria-describedby="emailHelp formMessage" による補足情報も確認します。計算されたプロパティ一覧において、説明(Description)に「架空のアドレスで練習できます。入力は外部に送信しません。」および「未確認です。」が連結されているかを記録します。

操作に応じた状態の変化を追跡する

フォームとDOMの章で掲載したイベントハンドラーを確認し、次の項目を追試します。

フォーム送信処理では、入力不正時に email.setAttribute('aria-invalid', 'true') が実行され、#formMessage がエラー文言へ書き換わります。空欄送信時に aria-invalid が「true」へ変化し、フォーカスが #email へ移動するかを観察します。

トグルボタンでは、クリックにより aria-expanded が「false」から「true」へ更新され、テキストが「詳細を閉じる」に変化します。この教材のボタン操作では、クリックによって属性とテキストが更新されます。

なお、role="status" によるライブ領域の指定は、属性がDOMに存在し計算されたプロパティに反映されていることの確認にとどめます。実際の音声読み上げの出力可否はOSや支援技術の環境に依存するため、ブラウザー単体の観察から読み上げを断定することはできません。

装飾用絵文字と無関係な情報の遮断

装飾要素がアクセシビリティツリーに混入するとノイズになります。純粋な装飾要素はツリーから除外します。

<!-- demo/index.html の抜粋 -->
<div class="product">
  <div class="art" aria-hidden="true">☕</div>
  <div>
    <h3>朝のコーヒー</h3>
    <p>長い説明文でも内容を読めるカードを作ります。</p>
  </div>
</div>

コーヒーカップの絵文字「☕」を囲む div には aria-hidden="true" が指定されています。Accessibilityペインで検証すると除外(Ignored)と表示され、支援技術向けの情報から遮断されていることが裏付けられます。属性が欠落していると絵文字がテキストとして登録され、見出しと重複する冗長な案内を生みます。

自動チェックの範囲と手動確認の境界

自動チェックと手動確認の守備範囲を整理します。

検査対象 自動チェックの範囲 人の手による照合範囲 確認項目
入力欄の名前 for と id の紐付け有無 表示文言とラベルの一致 Computed Nameと算出元要素
プレースホルダー 属性値の構文チェック ラベル代用になっていないか 名前に誤採用されていないか
トグルボタンの状態 aria-expanded の属性妥当性 操作後の状態と値の整合性 操作前後の属性変化とDOM更新
エラー通知 aria-invalid 等の属性形式 警告文と入力欄の関連性 バリデーション時の属性とフォーカス
装飾絵文字 属性の有無 隠蔽すべき装飾かの判断 AccessibilityペインでのIgnored状態

構文チェックはツールに委ねつつ、文脈上の妥当性や状態変化はペインで直接確かめます。

観測事実を分離した修正報告の作成

調査結果を報告する際は、以下の机上の例のように観測事実と提案を分離して記述します。

<!-- 報告記述の机上の例 -->
- 対象要素: `#toggle`
- 観測された状態: ボタンテキスト「詳細を表示」に対し、属性が aria-expanded="true" のまま不一致
- 期待される挙動: 展開状態と属性値が連動していること

修正案では、表示と属性のどちらが現在の状態を誤って表しているかを先に調べます。属性だけを一律に書き換えると表示との不整合が残るため、状態を更新するハンドラーとの対応を確認してください。

11. Consoleの情報から次の確認を選ぶ

Webページで不具合が起きたとき、最初にConsoleパネルを開く場面は多くあります。しかし、Consoleに記録された情報と画面上の症状の間には、時間的なずれや処理の食い違いが生じます。出力された記録の性質を慎重に腑分けし、次の確認手順への手がかりとして扱う必要があります。

エラーの発生時刻と画面の症状を結び付ける

Consoleのメッセージを追う際は、操作と時刻の前後関係を整理します。操作直前の表示状態を起点とし、クリックやキー入力を行った瞬間に何行の記録がConsoleへ追加されたかを観察します。操作と同時にスタックトレースを伴う例外が表示されたのであれば、その操作に対応するイベントハンドラーの実行中に問題が生じたと推測できます。一方で、操作より前に出ていたログは別の要因による副次的な例外の可能性があります。

公式の解説であるChrome DevTools Console Referenceが示すとおり、DevToolsのConsoleは重要度に応じたログのフィルタリングや、例外が発生した経路をたどるスタックトレースの展開機能を提供しています。ログレベル(Verbose、Info、Warnings、Errors)の選択状態によって出力が隠されていないかもあわせて確認します。

ログが出力されない原因をコードの構造から読む

JavaScriptが例外をcatchした後にログを出さない構造では、捕捉した例外の内容がConsoleに残らないことがあります。教材はHTMLとJSONの2ファイルをHTTPサーバー(http://127.0.0.1:9033/demo/?case=network)で配信して通信を行います。以下のJavaScriptコードを確認してみます。

これは demo/index.html の抜粋です。

対応する教材ハンドラーは前の実習章で掲載しています。ここでは処理の分岐とログ出力の有無を読み直します。

この処理において、失敗ボタンを押して missing-product.json を要求したとき、HTTPの応答は404になります。しかしコード側では if(!response.ok) によって分岐し、画面内の #networkResult 要素のテキストを書き換えて return しています。HTTPステータスコードが404であっても fetch のPromise自体は正常に解決されるため、プログラムの例外としては扱われません。アプリがconsole.errorを呼んでいなくても、ブラウザー自身が404などのネットワークエラーをConsoleに表示する場合があります。HTTPのエラー表示と、アプリが投げた例外を区別してください。

また、通信自体が失敗して catch 節が実行された場合も、画面に案内文を代入するのみでエラーオブジェクトを出力していません。このcatch自身はConsoleへ情報を出しません。ただし、ブラウザーによる通信エラーの表示まで抑止するわけではありません。

アプリケーションのコードと拡張機能のノイズを区別する

Consoleのエラーには、ブラウザー拡張機能がページ内に注入したスクリプト由来のものが混入することがあります。区別の基準となるのは、スタックトレースの右端に表示されているスクリプトの送信元やURLです。chrome-extension:// で始まるスクリプトは拡張機能由来だと判断する手掛かりになります。ハッシュ付きのファイル名だけでは、拡張機能かアプリかは判断できません。調査対象のオリジンを指す記録についても、呼び出し元のコードや関連する要求を照合します。拡張機能の動かない条件と比較するときは、シークレットモードでも実行を許可された拡張機能がないかを確認し、ログイン状態やキャッシュ条件の違いも記録します。

Consoleの調査手順と次の確認の対応

Consoleの観測結果から、次にどの機能やコードへ進むべきかを整理した表が以下です。

Consoleの観測結果 推定される状況 次に確認する場所や操作
赤色エラー(404 Not Found) 要求したリソースがサーバー側に存在しない NetworkパネルでリクエストURLとパス指定を照合する
赤色エラー(TypeError等) nullやundefinedに対するプロパティ参照などの可能性 Sourcesパネルでスタックトレースの該当行を開き、変数の内容を確認する
エラーなし(画面表示のみ失敗) response.ok による条件分岐や catch で握りつぶし 対象ハンドラーのJavaScriptコードを読み、分岐条件を追跡する
エラーなし(画面変化もなし) イベントリスナーの未登録、またはセレクターの不一致 Elementsパネルで要素のイベント設定やid属性の綴りを照合する
chrome-extension:// からの警告 ブラウザー拡張機能による干渉 シークレットウィンドウで再操作し、再現性を確認する

Consoleの役割は原因の最終決定ではなく、次に向かうべきパネルを決定するための手がかりです。

Console操作を用いた追試案とDOMの安全な確認

教材のDOM切り替え処理(http://127.0.0.1:9033/demo/?case=dom)において、ボタンを押しても詳細要素が期待通りに展開されない不具合を想定します。教材の該当箇所では次のコードが動いています。

これは demo/index.html の抜粋です。

対応する教材ハンドラーは前の実習章で掲載しています。ここでは処理の分岐とログ出力の有無を読み直します。

この処理の状態を確認する際、ページの状態を変更せずに読み取る追試案として、以下のスクリプトが考えられます。

提案コード(机上の例・未実行):

// ボタン要素の属性と関連先要素の現在の状態を非破壊で取得する案
console.log({
  expanded: document.querySelector('#toggle')?.getAttribute('aria-expanded'),
  childCount: document.querySelector('#detail')?.childElementCount,
  detailText: document.querySelector('#detail')?.textContent
});

この提案コードは机上の例であり、実際の観測記録とは異なります。値の書き換えを行わず読み取り専用プロパティに限定して確認することで、調査操作による状態破壊を防ぎます。本教材のボタン操作の挙動確認においても、属性の直接書き換えではなくボタンクリック操作によって状態を遷移させることが確実です。

入力内容の検証処理におけるConsoleの位置付け

教材のフォーム処理(http://127.0.0.1:9033/demo/?case=form)では、HTML5標準の validity プロパティを参照して分岐を行っています。

これは demo/index.html の抜粋です。

対応する教材ハンドラーは前の実習章で掲載しています。ここでは処理の分岐とログ出力の有無を読み直します。

ユーザーが空欄のまま送信ボタンを押した際、event.preventDefault() によってブラウザー本来の送信を止め、独自の文言を #formMessage に表示します。このハンドラーはエラー文をConsoleへ出力していません。実際のConsoleに何もないかは別途確認する必要があります。画面上にメッセージが出ているにもかかわらずConsoleが無反応である状況は、このようなガード節が機能した結果です。

Consoleの情報を過信せず次の段階へ進む考え方

Consoleに表示された文字列は、実行環境が処理を継続できなくなった地点や、明示的に出力させた地点を指しています。真の原因がその手前の代入ミスや呼び出し元からの引数にある場合、エラー箇所だけの修正では根本的な解決になりません。

発生時刻の照合、拡張機能ノイズの選別、コードによる例外捕捉の有無を確認した後は、Sourcesパネルでのブレークポイント設定やステップ実行へ進み、変数値の遷移を追跡します。

12. 処理を止めて状態の変化を調べる

画面の表示が変化した後からでは、JavaScriptの処理順序や途中で変数がどう入れ替わったかを正確に復元できない場面があります。処理を途中で一時停止させ、その瞬間に保持されている変数の値やDOMの状態を直接観察する手法がブレークポイントです。

公式ドキュメントのPause code with breakpointsで解説されているように、Chrome DevToolsのSourcesパネルではコード行への配置やイベント発生に合わせた停止が可能です。本章ではSourcesパネルを活用し、内部状態の変化を切り分ける手順を確認します。

この章は未実施の発展演習です。以下の停止時の値は教材ソースから予想した値であり、Sourcesパネルで取得した実測ログではありません。

停止箇所と変数の観測計画をあらかじめ決める

実行を止める前に、どの行で止め、スコープ内のどの変数に何の値が入ることを期待するか仮説を立てておきます。変化が起きる直前の値と直後の値を比較できる位置を選びます。教材のJavaScriptは行を詰めているため、行番号だけでは複数文のどこで止まるかを特定できません。停止位置の強調表示と変数の有効範囲を確認します。

調査対象の機能 設定する停止箇所 停止時に確認する対象と期待値 停止直後に変化する状態
詳細の開閉(DOM) const open = button.getAttribute(...) の直後 変数 open が論理値(trueまたはfalse)であること aria-expanded 属性とボタンの表示テキスト
詳細の開閉(DOM) detail.replaceChildren() の実行直前 要素 #detail の子要素数が0または既存要素であること #detail 内への <p> 要素の追加または消去
データ通信(Network) if (!response.ok) の判定行 response.status が200番台か400番台か、response.ok の真偽 成功用表示かエラー用表示クラス bad の付与
フォーム検証(Form) const valid = email.validity.valid の直後 email.value の入力文字列と validity オブジェクトの各フラグ aria-invalid 属性値とエラーメッセージの出力内容

停止させた瞬間の変数確認を整理しておくことで、何が期待通りでどこから想定が崩れたのかを即座に判定できます。

開閉処理におけるDOM操作前後の状態比較

教材の「DOM」画面(http://127.0.0.1:9033/demo/?case=dom)では、ボタンを押すことで詳細説明が開閉します。コードは demo/index.html からの抜粋です。

DOMの章で掲載したtoggleのクリックハンドラーを調べます。

ボタンのクリック直後に停止させ、変数 open の値が決まる瞬間を確認します。初期状態のHTMLでは aria-expanded="false" のため、button.getAttribute('aria-expanded') === 'true' の評価結果として変数 open には false が代入されます。Scopeペイン上で button と open: false が確認できます。

次の文へ進めて button.setAttribute('aria-expanded', String(!open)) を通過させると、ElementsパネルのDOMツリー上で aria-expanded が "true" に更新されます。detail.replaceChildren() の通過時点では #detail 領域は空です。if (!open) ブロック内の detail.append(p) が実行された瞬間にDOMへ段落要素が挿入されます。実際に画面へ描かれるタイミングは、DOMを更新した瞬間とは区別してください。

通信処理の分岐点における真偽値とレスポンスの捕捉

「通信」画面(http://127.0.0.1:9033/demo/?case=network)における非同期通信の分岐を検証します。

通信とフォームのハンドラーは前の章で掲載した教材コードを参照してください。

非同期の fetch を伴う処理では、応答のヘッダーを受け取ってResponseが返った後の if (!response.ok) の行にブレークポイントを配置します。

「商品データを取得」ボタンを押した場合、停止時の response は status: 200 かつ ok: true です。条件式 !response.ok は偽となり、await response.json() へ進みます。「存在しないデータを取得」ボタンを押した場合は response.status が 404、ok: false となります。条件式が真となってエラー表示と bad クラス付与が行われ、return で後続の処理が中断されます。

フォーム検証時のvalidityプロパティと属性の連動

「フォーム」画面(http://127.0.0.1:9033/demo/?case=form)では、送信イベント時に入力値の妥当性をブラウザの標準APIで判定しています。

通信とフォームのハンドラーは前の章で掲載した教材コードを参照してください。

const valid = email.validity.valid の直後にブレークポイントを置きます。空のまま送信して停止させた場合、Scopeペインの email.validity で valid: false および valueMissing: true が確認できます。

行を進めると要素に aria-invalid="true" が設定され、三項演算子により「メールアドレスを入力してください。」が出力されます。なお、入力欄に abc と入力して送信した場合は valueMissing: false かつ typeMismatch: true となり、「メールアドレスの形式を確認してください。」に切り替わります(これは机上の動作例です)。

停止処理がタイマーやイベント順序に与える影響

処理を一時停止させる性質上、時間経過に依存する挙動には副作用が生じます。変数を観察している間も時間は経過しています。

画面内に setTimeout や setInterval が存在する場合、停止と再開によって、通常時とはタイマーや通信完了を処理するタイミングが変わる可能性があります。通信のタイムアウトを設定している処理では、待機時間を超過して再開時にタイムアウト例外が発生することもあります。

停止中にDevToolsへフォーカスを移す操作自体が、調査対象の入力状態へ影響する場合もあります。

複数クリックによる競合と排他制御の検討

教材の「通信」画面でボタンを素早く連続クリックした場合、最初の fetch の完了前に2回目が開始されます。環境によって2回目の応答が先に解決し、遅れて1回目のレスポンスが到着すると画面が上書きされる競合が生じます。

対応案の一つは、取得中フラグと対象ボタンの非活性化で後続の開始を抑える方法です。今回、連続クリックの競合は実測していません。以下は改善案の実装例です(本章の解説用であり未実行のコードです)。

この案では、開始時に取得中フラグを立て、成功時も失敗時もfinallyで解除します。対象の二つの取得ボタンだけを無効化し、完了後に元の状態へ戻す設計を考えます。中断した要求や、画面を離れた後に完了した要求の扱いは別の課題です。

この提案を試す場合は、既存ボタンのハンドラーを新しい制御へ接続し直し、対象ボタンの選び方も確認します。関数を追加するだけでは既存のload呼び出しは変わりません。この処理でも、fetch 直前にブレークポイントを置いてボタンの disabled 属性や isLoading フラグが true を維持しているかを確認できます。

13. 再現できる調査報告へまとめる

ブラウザー上で問題の原因らしき箇所を突き止めたら、作業者の手元で起きた事象を別の開発者が同じ順序で追試できる調査報告票としてまとめます。 調査報告には、観測時の前提条件、再現手順、得られた事実、立てた仮説、試行した変更、そして未確認の境界を記述します。

調査環境と初期状態の確定

再現性の高い報告を作成するため、実行環境の境界を客観的な数値として記録します。OSの種別、ブラウザーの正確なバージョン番号、表示確認時のビューポート幅を明記します。

今回の検証環境は、macOS上で動作するGoogle Chromeです。細かなブラウザーの版番号とCSSピクセル単位のビューポート寸法は記録していません。画像ファイルの寸法はevidence.jsonに記録していますが、表示条件全体を表す値ではありません。画面は手元のローカル環境でHTTPサーバーを起動し、HTMLとJSONの2ファイルを配信して表示しています。 次のコードは、調査対象となった教材画面の骨格を定義しているdemo/index.htmlからの抜粋です。

<div class="frame">
  <div id="product" class="fixed">
    <div class="product">
      <div class="art" aria-hidden="true">☕</div>
      <div>
        <h3>朝のコーヒー</h3>
        <p>長い説明文でも内容を読めるカードを作ります。</p>
      </div>
    </div>
  </div>
</div>
<p id="layoutStatus" class="result">カード:width: 520px。親の内容領域を超えています。</p>

親要素<div class="frame">の内側に、初期状態でfixedクラスを持つ<div id="product">が配置されています。親要素には境界を示す点線枠が設定され、幅はスタイルシートで制限されています。

固定幅カードのはみ出しを追試する手順

事象を第三者が再現できるよう、作業者が行った操作を時系列の再現手順として記述します。

  1. ローカルサーバーを起動し、ブラウザーで http://127.0.0.1:9033/demo/index.html?case=responsive にアクセスします。
  2. 画面内に表示される茶色の点線枠で囲まれたカード要素を確認します。
  3. カード内の文章の右側が表示領域の境界で切れていることを確認します。
  4. 画面上に配置された「親の幅に収める」ボタンをクリックし、カードの横幅が枠内に収まる変化を確認します。
  5. 追加の追試として「固定幅に戻す」をクリックし、元の表示へ戻るか確認します。この戻す操作は今回の撮影記録には含みません。

観測事実と構造分析

再現手順を実行したあと、画面上で起きた事象を客観的な事実として記録します。

初期状態では、茶色の点線で示された幅360pxのコンテナ枠に対して、商品カード要素が右側へはみ出していました。状態表示欄には「カード:width: 520px。親の内容領域を超えています。」と出力されていました。見出し「朝のコーヒー」に続く説明文の一部が、右側の表示領域から見えなくなっていました。文章自体はカード内でも折り返しており、単純に改行がなかったわけではありません。

demo/index.html内で定義されている関連スタイルは次の通りです。

.frame{width:360px;max-width:100%;border:2px dashed #9b6848;padding:12px;overflow:auto}
.fixed{width:520px;background:#e3eff3;padding:20px}
.responsive{width:100%;max-width:520px;background:#e3eff3;padding:20px}

親要素.frameの横幅が360pxに制限されているのに対し、子要素.fixedにはwidth: 520pxが与えられています。親要素の内容領域よりも子要素の指定幅が大きいため、子要素は親の境界を越えてあふれ出します。親の幅360pxと子の固定幅520pxの不一致を報告書に明記します。

仮説の立案と検証の変更

観測結果を踏まえ、「子要素の横幅を親要素の内容領域に追従させつつ、画面が広いときだけ上限幅を適用すれば、はみ出しを防げる」という仮説を立てて検証します。

教材画面のボタン操作により、カード要素のクラス名.fixedを.responsiveへ切り替えました。このクラスにはwidth: 100%およびmax-width: 520pxが指定されています。 変更の結果、カードの横幅は親要素の内容幅332px(360pxから左右の枠線4pxとpadding24pxを除いた領域)に合わせて縮小し、内部の文章も枠線の手前で自然に折り返されました。状態表示欄の文言も「カード:width: 100%; max-width: 520px。親の内容領域に収まります。」へ更新されました。

同じ文章と親要素の条件を維持して幅指定を変えた結果、掲載した画面のはみ出しは解消しました。別の長さの文章や狭い親要素でも収まることまで、この結果だけで保証することはできません。

ソース修正と一時編集の違い

画面上でクラスを付け替える操作は、メモリ上に展開されたDOMやスタイルルールに対する一時編集です。ページを再読み込みすれば元の状態に戻ります。

開発チームが本番環境へ反映すべき作業は、リポジトリ管理下にあるHTMLやCSSファイルを書き換えるソース修正です。調査報告には「どのファイルの何行目をどのように書き換えるべきか」という具体的な差分指示を記載します。

Chrome DevTools の Local Overrides 機能を使用すると、ネットワーク経由で取得したファイルの内容をローカルディスク上のファイルで上書きし、再読み込み後も変更を保持したまま検証できます。ただし、手元のブラウザー内で応答を疑似的に置き換える仕組みであり、Webサーバー上の元ファイルを更新するわけではありません。ソースコード本体への反映手順は別途まとめる必要があります。

画面前後の記録と機密情報のマスキング

修正の効果を伝えるため、変更前と変更後の画面状態を提示します。 報告票に画像を添付する際は、個人情報や環境固有の機密文字列を塗りつぶすか、ダミーデータを用いた再現環境で撮影します。今回の教材画面は実在しない架空の商品名とローカル完結のスタイル構造のみを使用しており、外部サーバーへの通信や個人情報の露出は発生しません。

恒久対策の提案コード

調査結果から導き出された修正案をコードの形で提示します。次のCSS定義は、固定幅によるはみ出しを解消するために作成した修正案です。

/* 提案コード(実環境でのビルド確認は未実施) */
.card-container {
  width: 100%;
  max-width: 520px;
  box-sizing: border-box;
}

このCSSコードは本番のビルド環境や実運用のスタイルシートへ組み込んでの動作確認は行っておらず、手元での検証結果に基づき提示された未実行の提案コードです。このクラス名は説明用です。実際のHTMLの対象クラスへ結び付けなければ適用されません。対象要素と未実行であることを明記します。

開発担当者への引き渡し項目

調査報告票には、以下の項目を整理して盛り込みます。

  • 対象画面とURL:問題が発生する正確なパスとクエリパラメータ(http://127.0.0.1:9033/demo/index.html?case=responsive)。
  • 発生箇所の要素:セレクタとクラス名。
  • 観測された数値:親要素の計算幅と子要素の固定幅の差異。
  • 推奨される修正方針:固定値からパーセンテージおよび最大幅指定への移行。
  • 影響調査の依頼:同一のクラスやスタイルを共通利用している別画面の有無の確認。

確認できなかったこと

手元の特定環境で再現と解消が確認できたからといって、あらゆる条件下で同じ結果が得られるとは限りません。

DevToolsのパネル操作とそこでの数値取得は今回実施しておらず、公式資料と教材ソースに基づく追試手順として記述しました。今回の検証環境はmacOS上のGoogle Chromeのみであり、WindowsやLinux環境、SafariやFirefoxなどの他ブラウザーでの表示差異は確認していません。スマートフォンの実機によるタッチ操作や画面回転時の挙動、高DPIディスプレイでの表示確認も未実施です。スクリーンリーダーによる読み上げ、ネットワーク速度制限下での読み込み順序の影響、ページの描画負荷やメモリ消費を測る性能計測も未確認です。

調査報告票の末尾には、こうした未確認の事項を明記する欄を必ず設けます。

記事一覧へ戻る