JEPA4Japan · チュートリアル

第14章:正しく見えることと、本当に正しいことは違う

7,561文字 19分で読めます #Canvas#Frontend Engineering#Infinite Canvas#ELI5

Model、State Machine、Browser、Visual、Accessibility、Performance、Production Telemetryの各層でCanvasエディターの正しさを証明します。

コース進捗 コース目次 18レッスン中 18件を公開中

第I部:描く前に描画面を選ぶ——プロダクト、ピクセル、座標

  1. 01 第1章:まだ描かない——Canvasはプロダクト設計ではない 公開中
  2. 02 第2章:すぐに記憶を失うピクセルの紙 公開中
  3. 03 第3章:お絵描きを再現可能なレシピにする 公開中
  4. 04 第4章:4枚の地図と1台のカメラ 公開中

第II部:ピクセル世界に頭脳を与える——モデル、スケジューリング、入力、ツール

  1. 05 第5章:ピクセル世界に台帳を作る 公開中
  2. 06 第6章:ランプが点いたときだけ描き直す——Render SchedulerとReactの境界 公開中
  3. 07 第7章:マウス、指、ペンに同じ言葉を話してもらう 公開中
  4. 08 第8章:細い縁を調べる前に大きな箱を探す 公開中
  5. 09 第9章:ツールは信号機であり、Booleanの袋ではない 公開中

第III部:「ドラッグできる」から「信頼できる」へ——操作、文字、Asset、復旧

  1. 10 第10章:触って気持ちよいエディターにする 公開中
  2. 11 第11章:描かれた文字は編集できる文字ではない 公開中
  3. 12 第12章:借りた画像を勝手に箱へ詰めてはいけない 公開中
  4. 13 第13章:タイムマシンと古い箱 公開中
  5. 14 第14章:正しく見えることと、本当に正しいことは違う 現在のレッスン

第IV部:マスターの判断——Performance、Worker、GPU、SDK、共同編集、AI

  1. 15 第15章:1万人を一人ずつ探さない 公開中
  2. 16 第16章:受付を厨房へ入れない——WorkerとGPUへの更新 公開中
  3. 17 第17章:車を自作するか、実績あるシャーシを買うか 公開中
  4. 18 第18章:人とAIが同じ台帳を編集する 公開中

5歳児にもわかるゲームから始めよう

この章の 1 つの真実: 正しく見える Canvas スクリーンショットは、その瞬間のピクセルが類似していたことだけを証明します。モデル、インタラクション、ビジュアル、アクセシビリティ、および本番動作はそれぞれ個別に証明する必要があります。

美しいおもちゃの消防車に乗ってみましょう。それに触れる前に、5 つのことを予測してください。車輪は回転できるか?テーブルから落とすと崩れてしまいますか?目を閉じてもブレーキを見つけることができますか?引きずらずに片手でドアを開けることができますか?先生が帰った後に壊れた場合、いつ壊れたかわかりますか?その真っ赤な体は、これらの質問のどれにも答えません。

次に、5 つの検査ステーションを作成します。まずは分解してギアルールを点検します。 2 回目では、一定の順序でボタンを押します。 3 番目では、異なるテーブルの上、異なる照明の下で運転します。 4つ目は目隠しをしてリモコンをキーボードのみに置き換えます。 5つ目は、「どのような種類の障害が発生し、どれくらいの時間がかかったのか」だけを記録するブラックボックスを設置します。各ステーションは異なる障害の証拠を生成し、異なる修正が必要になります。

  1. パートルールルールはどのように変わっても適用されます
  2. ボタンシーケンスキャンセルは誤ってコミットすることはありません
  3. 本物の卓上Browser とピクセル
  4. マウスなしタスクはまだ機能します
  5. 使用中の温度を確認してくださいプライベートなコンテンツを決して盗み見しないでください
まず予測します。すべてのビジュアル スナップショットが成功しても、Esc キーを押しても移動がコミットされる場合、製品は正しいでしょうか?いいえ、通過したのは 1 駅だけです。

テスト ピラミッドは、上位レイヤが「より高度」であることを意味するものではありません。下位レベルのルールは迅速に実行され、多くのケースを調査できます。 Browser テストは、実際の入力とプラットフォームの動作を証明します。視覚テストにより外観が証明されます。パフォーマンス回帰テストにより予算が証明されます。 Accessibility は最後に追加されるラベルではありません。これは、同じ Document へのフォーカス可能、記述可能、キーボード操作可能な代替パスです。

おもちゃを Canvas の概念に置き換える

おもちゃの消防車の点検Canvas Labの証拠質問に答える
歯車寸法ゲージGeometry / Model 単体テスト数学と不変条件は成り立つでしょうか?
ランダムに車輪を回転させるProperty-Based Testプロパティは多くの有効な入力にわたって保持されますか?
ボタンアクションのスクリプトEvent Replay / 状態アサーションどのトランジションが実行されましたか?
制御可能なストップウォッチDeterministic Clockダブルクリック、長押し、アニメーションはリアルタイムとは独立していますか?
実際のテーブルで試乗してみるBrowser Interaction テストPointer、IME、Clipboard は実際に機能しますか?
同じ角度からの写真を比較してくださいVisual RegressionDPR、フォント、または描画が予期せず変更されましたか?
目隠しをしているときにマーカーの隆起を感じるFallback Semantics / Focus MappingScreen Reader はオブジェクトを理解して位置を特定できますか?
矢印キー付きリモコンKeyboard Move / サイズ変更ドラッグせずにタスクを完了できますか?
大きなボタンTarget Size / Non-Drag Alternative低精度の入力は機能しますか?
ブラックボックスPrivacy-Safe Telemetryどこで生産が失敗し、どの程度の影響がありましたか?

たとえ話が止まったところで、実際の支援技術は「目隠しをして使用する」ほど単純ではありません。ユーザーには、視覚的、運動的、認知的、または複数の違いがある場合があります。 Screen Reader、キーボード、音声、ハイコントラストのニーズを 1 つのエクスペリエンスで表現することはできません。ブラック ボックスには、キャンバス テキスト、スクリーンショット、正確な軌跡、またはアセット URL も記録してはなりません。 Correlation ID は 1 つの障害チェーンを接続します。それは人を追跡する方法ではありません。

まず誤った思い込みを捨てる

  • **「一度クリックスルーしたので、テストは完了しました。」**Pointer Cancel、レース、DPR、IME の構成を一貫して再生することはできません。手動による調査は重要ですが、再現可能な証拠に代わることはできません。
  • **「100% のカバレッジは正確さを意味します。」**行を実行しても、Matrix Round Trip、Command Inversion、または Migration プロパティは証明されません。
  • **「スクリーンショットが一致するということは、インタラクションが正しいことを意味します。」**同じ最終ピクセルは、History の破損、フォーカスの破損、または元に戻せないアクションによって発生する可能性があります。
  • **「Canvas にはアクセスできないので、aria-label 1 つで十分です。」**1 つの大きなブラック ボックス ラベルでは、ユーザーが shape-7 を選択したり、その接続を理解したり、移動したり、サイズを変更したりすることはできません。
  • **「Keyboard のサポートとは、多くの Pointer Move を使用してドラッグをシミュレートすることを意味します。」**キーボードは偽のブラウザ イベントではなく、同じドメイン Command を発行する必要があります。 History、権限、監査の一貫性が維持されます。
  • **「Telemetry が詳細であればあるほど、デバッグが容易になります。」**生のテキスト、画像、完全な座標シーケンス、ユーザー ID によってビジネス コンテンツが漏洩します。カテゴリ、期間、カウント、大まかな環境データ、およびランダム相関 ID のみを収集します。
  • **「テストは Chromium でのみ実行する必要があります。」**Clipboard、IME、フォント、タッチ、グラフィックスのパスはブラウザーによって異なります。実際のユーザーとリスクから Browser Matrix を選択してください。

本番環境に必要な知識

前提条件

第 8 章の GeometryKernel は、Canvas コンテキストに依存してはなりません。第 9 章の ToolMachine は Fake Input Stream を受け入れる必要があります。第 13 章の Command、History、Migration、および永続化には純粋関数境界が必要です。 Renderer を削除した後にこれらのモジュールをインポートできない場合は、アーキテクチャが事実をピクセルに貼り付けています。テストを追加する前にその境界を分離してください。

体系的な知識

**単体テストとプロパティテスト。**Matrix Round Trip は inverse(transform(p)) ≈ p をチェックします。 Geometry Invariants は、境界にジオメトリが含まれていること、距離が負でないこと、および交差が対称であることを確認します。 Transformed Bounds は、回転、負のスケール、イプシロンをカバーします。 Command Inversion は、正当なリビジョン メタデータを無視して、apply(invert(apply(doc, c))) = doc を満たす必要があります。 Migration は、フィクスチャ、べき等性、破損の分離をテストします。 Serialization は、偶発的なオブジェクトとプロパティの順序ではなく、安定したフィールドをテストします。 Property-Based Testing は、多数の有限数、有効なドキュメント、およびイベント シーケンスを生成し、失敗を最小の反例に縮小します。

**状態およびイベントのテスト。**Event Replay は、第 7 章の正規化されたイベントの配列を ToolMachine に送信します。 State Transition Assertions は、最終ピクセルのみをチェックするのではなく、{from,event,to,effect} を一緒に検証します。 Pointer Cancel、Lost Capture、Blur、リモートでのオブジェクト削除、Undo リクエストを組み合わせて Race Sequence を構成します。テストは Deterministic Clock を手動で進め、不安定な setTimeout(300) の障害を回避します。 Fake Input Stream は DOM イベントを構築しないため、ノードで実行できます。

**ブラウザテスト。**Playwright または同等のツールは、実際の Pointer、Keyboard、Clipboard 権限、IME コンポジション、ドラッグ/ドロップ、および Export ダウンロードを検証します。 Visual Regression は、ブラウザのバージョン、フォント、ビューポート、DPR、配色、およびアニメーション クロックを修正します。すべての差異のしきい値は説明可能でなければなりません。テストが失敗するたびにベースラインを更新しないでください。ユーザー データに基づくと、Browser Matrix は少なくとも Chromium、Firefox、WebKit をカバーし、確実にシミュレートできないパスについては実際のタッチ デバイスとペン デバイスを備えている必要があります。

アクセシビリティ。 Canvas の外側に DOM Inspector as an Alternate Interface を構築します。操作可能なすべての Shape には、意味論的な役割、名前、関係の説明、および安定した data-shape-id があります。 Focus Mapping は、Canvas 選択と DOM 行を両方向に同期します。リモート変更は焦点を奪いません。 Keyboard Selection は、Tab、矢印キー、および複数選択をサポートします。 Keyboard Move/Resize は、Pointer 入力と同じコマンドを発行します。 Non-Drag Alternative として数値入力またはボタンを提供します。クリック可能なハンドルの視覚的なサイズを、インタラクティブな Target Size から分離します。 Screen-Reader Description は、ピクセルを読み上げるのではなく、タイプ、名前、位置、接続、および状態を説明します。 Reduced Motion は不要な補間を停止します。 High Contrast は色だけでは Selection を識別しません。

可観測性。 Uncaught Error、未処理の拒否、Render Time、Slow Interaction、および保存/Migration/Asset エラーを、オブジェクト数、表示数、ズーム バケット、入力デバイス、Browser バージョン、Correlation ID とともにキャプチャします。 Long Task エントリとカスタム パフォーマンス マークは、インタラクションを入力、モデル、ジオメトリ、レンダリング、および現在のステージに分割します。 Privacy-Safe Telemetry は、Shape テキスト、スクリーンショット、完全な URL、ユーザー名、および正確な軌跡を禁止します。サンプリング レート、保持期間、およびオプトアウトには製品ポリシーが必要です。

完全なテスト ピラミッドは、フィードバック速度に従って順序付けされています: Geometry/Model ユニット → ステート マシン → エンジン統合 → Browser Interaction → Visual Regression → Performance Regression。レイヤーを上に追加するだけでは構築されません。すべての本番環境の障害は、早期かつ確実に再現できる最下層で回帰テストを受ける必要があります。

根拠と互換性(確認日:2026-08-29)

Canvas フォールバック コンテンツとアクセシビリティに関する考慮事項については、WHATWG Canvas 要素 に従ってください。キーボード操作と可視フォーカスについては、WCAG 2.2 Keyboard および 可視フォーカス を参照してください。ターゲットの寸法については、WCAG 2.2 Target Size (最小) を参照してください。 Reduced Motionはprefers-reduced-motionを使用します。実行時メトリクスは PerformanceObserver を通じて取得できますが、最初に各エントリ タイプを機能検出します。自動アクセシビリティ スキャンは、Screen Reader およびタスク レベルの手動テストに代わることはできません。

本章の実装ステップ

**出発点:**第 13 章には回復可能な Document がありますが、検証は依然として開発者がいくつかのものをドラッグし、スクリーンショットを確認することで構成されています。**フィニッシュライン:**6 層の自動化された証拠と DOM インスペクター。ユーザーはマウスを使わずに選択、移動、サイズ変更、削除を行うことができます。本番環境の失敗には、プライバシーに配慮した段階のメトリクスが含まれます。

次のファイルとインターフェイスを追加します。

  • src/engine/testing/FakeInputStream.ts および ManualClock.ts: 決定論的なイベント。
  • src/engine/__tests__/properties.test.ts: マトリックス、ジオメトリ、Command、および Migration プロパティ。
  • src/ui/CanvasAccessibility.ts: DOM インスペクターおよび Focus Mapping;
  • tests/browser/editor-a11y.spec.ts: Keyboard、Clipboard、IME、および DPR。
  • tests/visual/canvas.spec.ts: 固定環境でのスクリーンショット。
  • src/observability/telemetry.ts: ステージ マーク、エラー カテゴリ、編集。

以下の HTML/TypeScript は、Canvas に操作可能な代替インターフェイスを提供します。すべてのピクセルが DOM に変換されるわけではありません。ビジネス オブジェクトを Document からリストに投影します。ボタンはドメイン コマンドを直接発行するため、Pointer と Keyboard は History と権限を共有します。ビルド ツールは TypeScript をコンパイルし、外部モジュールとしてロードすることに注意してください。 type Shape = ... をブラウザネイティブの に直接置くことはできません<script>:

<main class="canvas-shell">
  <canvas id="canvas" tabindex="0" aria-label="ワークフローキャンバス" aria-describedby="canvas-help"></canvas>
  <p id="canvas-help">Tab キーでオブジェクト一覧に移動します。矢印キーで移動し、Shift キーで移動量を増やし、+/- でサイズを変更し、Delete キーで削除します。</p>
  <section aria-labelledby="objects-title">
    <h2 id="objects-title">キャンバス上のオブジェクト</h2>
    <ul id="object-list"></ul>
    <p id="canvas-status" role="status" aria-live="polite"></p>
  </section>
</main>
<script type="module" src="/src/ui/CanvasAccessibility.ts"></script>
type Shape = {
  id: string;
  type: 'rect' | 'text';
  name: string;
  x: number;
  y: number;
  w: number;
  h: number;
};
type Command =
  | { type: 'MoveShape'; id: string; dx: number; dy: number }
  | { type: 'ResizeShape'; id: string; dw: number; dh: number }
  | { type: 'DeleteShape'; id: string };

const shapes: Shape[] = [
  { id: 'shape-start', type: 'rect', name: '開始', x: 20, y: 40, w: 120, h: 64 },
  { id: 'shape-review', type: 'text', name: '手動レビュー', x: 220, y: 40, w: 160, h: 64 },
];
const selected = new Set<string>(),
  undoStack: Shape[][] = [];
const list = document.querySelector<HTMLUListElement>('#object-list')!;
const status = document.querySelector<HTMLElement>('#canvas-status')!;
const canvas = document.querySelector<HTMLCanvasElement>('#canvas')!;

function execute(command: Command) {
  const i = shapes.findIndex((s) => s.id === command.id);
  if (i < 0) return;
  const before = structuredClone(shapes);
  if (command.type === 'DeleteShape') {
    shapes.splice(i, 1);
    selected.delete(command.id);
  } else if (command.type === 'MoveShape')
    shapes[i] = { ...shapes[i], x: shapes[i].x + command.dx, y: shapes[i].y + command.dy };
  else
    shapes[i] = {
      ...shapes[i],
      w: Math.max(24, shapes[i].w + command.dw),
      h: Math.max(24, shapes[i].h + command.dh),
    };
  undoStack.push(before);
  status.textContent = `${command.type} が完了しました。元に戻せます`;
  renderInspector(command.type === 'DeleteShape' ? undefined : command.id);
  drawCanvas();
}
function undo() {
  const before = undoStack.pop();
  if (!before) return;
  const focusedId = (document.activeElement as HTMLElement | null)?.dataset.shapeId;
  shapes.splice(0, shapes.length, ...before);
  status.textContent = '直前の操作を元に戻しました';
  renderInspector(focusedId);
  drawCanvas();
}
function description(s: Shape) {
  return `${s.type === 'text' ? 'テキスト' : '矩形'}「${s.name}」、位置 ${s.x}, ${s.y}、サイズ ${s.w} × ${s.h}`;
}
function renderInspector(preferredFocusId?: string) {
  const focusedId =
    preferredFocusId ?? (document.activeElement as HTMLElement | null)?.dataset.shapeId;
  list.replaceChildren(
    ...shapes.map((s) => {
      const li = document.createElement('li'),
        button = document.createElement('button');
      button.type = 'button';
      button.dataset.shapeId = s.id;
      button.textContent = s.name;
      button.setAttribute('aria-pressed', String(selected.has(s.id)));
      button.setAttribute('aria-label', description(s));
      button.onclick = () => {
        selected.clear();
        selected.add(s.id);
        renderInspector(s.id);
        drawCanvas();
      };
      button.onkeydown = (event) => {
        const step = event.shiftKey ? 10 : 1;
        const delta: Record<string, [number, number] | undefined> = {
          ArrowLeft: [-step, 0],
          ArrowRight: [step, 0],
          ArrowUp: [0, -step],
          ArrowDown: [0, step],
        };
        if (delta[event.key]) {
          event.preventDefault();
          const [dx, dy] = delta[event.key]!;
          execute({ type: 'MoveShape', id: s.id, dx, dy });
        } else if (event.key === 'Delete') {
          event.preventDefault();
          execute({ type: 'DeleteShape', id: s.id });
        } else if (event.key === '+' || event.key === '=') {
          event.preventDefault();
          execute({ type: 'ResizeShape', id: s.id, dw: step, dh: step });
        } else if (event.key === '-') {
          event.preventDefault();
          execute({ type: 'ResizeShape', id: s.id, dw: -step, dh: -step });
        }
      };
      li.append(button);
      return li;
    }),
  );
  if (focusedId)
    list.querySelector<HTMLElement>(`[data-shape-id="${CSS.escape(focusedId)}"]`)?.focus();
}
function drawCanvas() {
  const ctx = canvas.getContext('2d')!;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (const s of shapes) {
    ctx.strokeStyle = selected.has(s.id) ? '#2563eb' : '#334155';
    ctx.strokeRect(s.x, s.y, s.w, s.h);
    ctx.fillText(s.name, s.x + 8, s.y + 24);
  }
}
addEventListener('keydown', (event) => {
  if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
    event.preventDefault();
    undo();
  }
});
matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', drawCanvas);
renderInspector();
drawCanvas();

おもちゃの話に戻ります。DOM リストは、トラック上の触覚バンプと名前付きボタンのセットであり、別のトラックではありません。同じ shapes 上で動作するため、晴眼者のユーザーと Screen Reader ユーザーは、2 つの異なるビジネス ロジック セットに入ることがありません。

プロパティ テストとブラウザ テストは、数学とタスクを個別に証明します。

import fc from 'fast-check';
import { expect, test } from 'vitest';

test('純粋な数学カーネルの可逆な平行移動・回転・拡大縮小はすべて往復変換できる', () => {
  fc.assert(
    fc.property(
      fc.record({
        x: fc.double({ noNaN: true, min: -1e6, max: 1e6 }),
        y: fc.double({ noNaN: true, min: -1e6, max: 1e6 }),
      }),
      fc.double({ noNaN: true, min: 0.01, max: 100 }),
      fc.double({ noNaN: true, min: -Math.PI, max: Math.PI }),
      (point, scale, angle) => {
        const c = Math.cos(angle),
          s = Math.sin(angle);
        const m = { a: c * scale, b: s * scale, c: -s * scale, d: c * scale, e: 37, f: -19 };
        const apply = (p: { x: number; y: number }, n: typeof m) => ({
          x: n.a * p.x + n.c * p.y + n.e,
          y: n.b * p.x + n.d * p.y + n.f,
        });
        const det = m.a * m.d - m.b * m.c;
        const inverse = {
          a: m.d / det,
          b: -m.b / det,
          c: -m.c / det,
          d: m.a / det,
          e: (m.c * m.f - m.d * m.e) / det,
          f: (m.b * m.e - m.a * m.f) / det,
        };
        const screen = apply(point, m),
          roundTrip = apply(screen, inverse);
        expect(Math.abs(roundTrip.x - point.x)).toBeLessThan(1e-6);
        expect(Math.abs(roundTrip.y - point.y)).toBeLessThan(1e-6);
      },
    ),
    { numRuns: 2_000 },
  );
});
import { expect, test } from '@playwright/test';

test('フォーカスと History を保ったままキーボードだけでオブジェクトを選択・移動できる', async ({ page }) => {
  await page.goto('/lab');
  const start = page.getByRole('button', { name: /矩形「開始」/ });
  await start.focus();
  await page.keyboard.press('ArrowRight');
  await page.keyboard.press('Shift+ArrowDown');
  await expect(start).toHaveAttribute('aria-label', /位置 21, 50/);
  await page.keyboard.press('ControlOrMeta+z');
  await expect(start).toHaveAttribute('aria-label', /位置 21, 40/);
  await page.keyboard.press('ControlOrMeta+z');
  await expect(start).toHaveAttribute('aria-label', /位置 20, 40/);
  await expect(start).toBeFocused();
});

Telemetry はランタイム許可リストも通過する必要があります。 TypeScript タイプは、as any、古いクライアント、またはサードパーティ SDK を停止できません。以下のシリアライザーは、カテゴリ、期間、カウント、および大まかな環境データのみを受け入れ、不明なフィールドを拒否します。これにより、shapeText、Screenshot、URL、座標、およびユーザー ID が「便宜上」紛れ込むのを防ぎます。

const allowed = new Set([
  'name',
  'correlationId',
  'durationMs',
  'objectCount',
  'visibleCount',
  'zoomBucket',
  'inputDevice',
  'browserFamily',
  'browserMajor',
]);
const eventNames = new Set([
  'uncaught_error',
  'slow_interaction',
  'save_failure',
  'migration_failure',
  'asset_failure',
]);

export function serializeTelemetry(raw: unknown) {
  if (!raw || typeof raw !== 'object') throw new Error('INVALID_TELEMETRY');
  const event = raw as Record<string, unknown>;
  for (const key of Object.keys(event))
    if (!allowed.has(key)) throw new Error(`SENSITIVE_OR_UNKNOWN_FIELD:${key}`);
  if (!eventNames.has(String(event.name)) || !/^[a-f0-9-]{36}$/.test(String(event.correlationId)))
    throw new Error('INVALID_TELEMETRY');
  for (const key of ['durationMs', 'objectCount', 'visibleCount', 'browserMajor']) {
    const value = event[key];
    if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value) || value < 0))
      throw new Error('INVALID_TELEMETRY_NUMBER');
  }
  return JSON.stringify(event);
}

correlationId は、障害チェーンごとに生成される、ランダムで有効期間の短い ID です。アカウント、ドキュメント、または Shape ID から派生したものであってはなりません。 Schema テストでは、{ ...safe, shapeText: '機密情報' }、screenshot、url、points、および userId がすべて拒否されることを確認する必要があります。サーバーは同じ許可リストを再度適用し、サンプリング、保持、アクセス制御、および削除のポリシーを適用する必要があります。

npm exec vitest run src/engine を実行します。 2,000 のマトリックス/Geometry プロパティ ケース、Command Inversion、Migration、および Serialization テストがすべて合格することが期待されます。 npm exec playwright test --project=chromium --project=firefox --project=webkit を実行します。 Pointer、Keyboard、拒否された Clipboard アクセス、IME、ドラッグ/ドロップ、Export、およびアクセシビリティ タスクが通過することが予想されます。 npm run test:visual を実行する場合は、ベースラインの差がゼロであることが予想されます。ベースラインを更新するすべての PR には、視覚的な理由を記載する必要があります。 npm run benchmark は p50/p95/p99 をレポートし、予算と比較する必要があります。

意図的に壊してみる

注入された失敗症状証拠修正回帰テスト回復
マウスのプラグを抜きます選択も移動もできないKeyboard タスクが停止するDOM インスペクター + 同一ソース CommandKeyboard専用劇作家フォーカスの保持と Selection
200% ズームの Browserハンドル/オーバーレイの位置がずれているScreenshot + バウンディングボックス画面/CSS座標を統一する100/200/400% プロジェクトオーバーレイを再計算する
Reduced MotionCameraはまだ飛んでいますメディアクエリとクロックトレース不要なアニメーションの継続時間を 0 に設定するエミュレーションテスト最終状態にジャンプ
フォントがありませんラッピングとSelectionシフトフォントステータス + 視覚的な差分明示的なフォールバック。フォント準備完了後の再レイアウトブロックフォントルート決定論的フォールバック
変更テスト DPRピクセルがぼやける、またはスナップショットがずれるバッキングサイズレポートDPR 対応のサイズ変更DPR 1/2/3 のスクリーンショットビットマップを再構築する
Clipboard 権限が拒否されました未処理の約束clipboard_denied テレメトリテキスト/ボタンのフォールバックを提供するPermission-拒否テストDocument は変更しないままにしておきます
Migration Payload 破損しています空白のページmigration_failure + バージョン隔離および回復 UI を開く破損した備品古いコミットされた状態を読み取る
Screen Reader には Canvas のみが表示されます「キャンバス」のみ発表Accessibility ツリーフォーカス可能なオブジェクトのリストと説明VoiceOver/NVDAタスクFocus Mapping

根拠を示して合格する

自動化された証拠手動による証拠合格条件
ユニット、プロパティ、リプレイ、Browser、ビジュアル、パフォーマンス CIVoiceOver/NVDA キーボード タスク、200% ズーム、High Contrast、タッチターゲット チェックすべての自動化されたレイヤーが通過します。手動タスクを完了 マウスなしで選択/移動/サイズ変更/削除
Telemetry Schema テストと敏感なフィールド除去テストテスト環境で 1 つの実際の相関チェーンを検査するステージと失敗のカテゴリは、テキスト、スクリーンショット、正確なパス、ID なしで検索可能です。
レイヤー証拠を渡す代替できない証拠
Model/Geometryプロパティの不変条件と最小限の反例スクリーンショット 1 枚
ステートマシンリプレイのすべてのトランジション最終的な Shape の位置
Browserリアル Pointer/IME/Clipboard/ドラッグ手書きの DOM イベントを使用した単体テスト
ビジュアル固定環境でのピクセル差分「私には大丈夫そうに見えます」
AccessibilityキーボードとScreen Readerタスクを完了する自動スキャンエラーゼロ
生産段階的 p95、分類されたエラー、および相関チェーン開発マシンコンソール
  • Rendererを削除した後も、Geometry、Document、History、Migration、State Machine、Commandsのテストをimportして実行できます。
  • Pointer Cancel、Race Sequence、Fake Input Stream、および Deterministic Clock にはすべてテストがあります。
  • Keyboard は、Non-Drag Alternative を使用して、選択、移動、サイズ変更、削除を行うことができます。
  • 再描画またはリモート更新中に理由もなくフォーカスが失われることはありません。 Selection と DOM Inspector は同期を維持します。
  • Reduced Motion、High Contrast、200% ズーム、および適切な Target Size は手動タスクに合格します。
  • Telemetry には、Shape テキスト、スクリーンショット、完全な URL、正確な軌跡、または身元情報は含まれません。

5歳児に説明する

「単体テスト」、「アクセシビリティ」、または「Telemetry」という言葉は使用しないでください。消防車の美しい写真が、車輪やブレーキが機能することや、視界がなくても使用できることを証明できない理由を説明してください。なぜどの検査ステーションも削除できないのでしょうか?実際の学校でトラックが故障した場合、子供の秘密を読み取らずに、どうやって有益な手がかりを残すことができるでしょうか?

専門用語を使わない適切な回答

写真からわかるのは、その瞬間の車体の外観だけです。部品を個別に測定し、あらゆる種類のボタンを押し、実際のテーブルで試運転する必要があります。また、マウスを使えない人や画面を見られない人でも、名前の付いたものを見つけて同じ操作を行えることを確認します。学校でトラックが遅くなったり故障したりした場合は、「どの部分にどれだけ時間がかかったか、どの種類の故障が起きたか、どの記録が同じ出来事に属するか」だけを残し、子どもが描いたものや書いたものは決して記録しません。こうして、「本当に正しい」の種類ごとに、それぞれの証拠を持たせます。