【PR】を含みます。

フロントエンド

【開発用備忘録】JavaScriptでフォームを一括自動入力するスニペット(select/radio/checkbox対応)

開発用備忘録 JavaScriptでフォームを一括自動入力するスニペット(select/radio/checkbox対応)

フォームの動作確認をしていると、毎回同じ入力を手で行うのが地味に面倒です。

そこで、DevTools(コンソール)に貼り付けるだけで フォームをまとめて自動入力 できるスニペットを用意しておくと、テストの速度が一気に上がります。

この記事では、inputtextareaselect(radio/checkbox含む)を対象に、値のセットだけでなくinput/changeイベントも発火する「実務テスト向け」スニペットをまとめます。(React/Vueなどで「値を入れただけだとUIが反応しない」ケースの対策にもなります)

この記事でわかること

  • フォーム自動入力スニペットでできること(select/radio/checkbox含む)
  • DevToolsに貼り付けて使う手順と、再実行・オプションの使い方
  • password / email / date / tel / number などtype別入力ルールのカスタマイズ方法

このスニペットでできること

対象要素

  • input / textarea / select を対象
  • 表示されている要素だけを入力対象にする(display:none などは除外)

入力ルール(type別)

  • text/textarea:test1, test2…のように連番で入力
  • password:password
  • email:test@example.com
  • date:今日の日付(YYYY-MM-DD
  • tel:09012345678
  • number:0

select / radio / checkbox の扱い

  • select:無効ではない最後のoptionを選択
  • radio:同じnameのグループ内で最後の要素だけを選択(他は解除)
  • checkbox:同じnameのグループ内で最後の要素だけをON(他はOFF)

input/changeイベントを発火する理由

単純にel.value = '...'で値を入れるだけだと、UI側(React/Vue等)が監視しているイベントが発火せず、「画面の表示」や「バリデーション」が更新されないことがあります。

このスニペットはinputchangeを両方発火することで、入力として扱われやすい形にしています。

DOMが増えるフォームにも対応(複数パス実行)

ステップフォームや条件分岐で、入力をきっかけに次の入力欄がDOMに追加されるケースがあります。

そのため、このスニペットでは「入力 → DOMの更新が落ち着くまで待機 → 再度入力」を複数回繰り返し、途中で追加された要素もまとめて埋められるようにしています。

使い方

DevToolsに貼り付けて実行

  1. ChromeなどのDevToolsを開く
  2. Consoleにスニペットを貼り付けて実行
  3. 完了するとログが出る(例:[fillInputsSmart] done:

再実行(window.fillInputsSmart)

実行するとwindow.fillInputsSmartが利用できるようになるので、必要に応じて手動で再実行できます。

window.fillInputsSmart();

オプション(maxPass / idleMs / timeoutMs / textStart)

DOMの追加が多いページや、待ち時間を調整したいときに使います。

window.fillInputsSmart({
  maxPass: 10,
  idleMs: 200,
  timeoutMs: 2000,
  textStart: 1
});

スニペット(コピペ用)

Copyをクリックするとコピーできます。

JavaScript
Copy
(() => {
  // 値をセットしただけではUIが反応しないケースがあるため、
  // input/change を両方発火して「ユーザー入力に近い挙動」に寄せる
  const dispatchInputEvents = (el) => {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
  };
  // input/textarea に値をセットしてイベントを発火する
  // disabled/readOnly は対象外、同じ値なら更新しない(無駄なイベント発火を避ける)
  const setValue = (el, value) => {
    if (!el || el.disabled || el.readOnly) {
      return false;
    }
    if (el.value === value) {
      return false;
    }
    // focus しておくと、フォーカス依存のUI/バリデーションが動く場合がある
    el.focus();
    el.value = value;
    dispatchInputEvents(el);
    return true;
  };
  // checkbox/radio の checked を切り替えてイベントを発火する
  // disabled は対象外、同じ状態なら更新しない
  const setChecked = (el, checked) => {
    if (!el || el.disabled) {
      return false;
    }
    if (el.checked === checked) {
      return false;
    }
    el.focus();
    el.checked = checked;
    dispatchInputEvents(el);
    return true;
  };
  // select は「無効ではない最後の option」を選択する(テスト用の簡易ルール)
  const selectLastOption = (selectEl) => {
    if (!selectEl || selectEl.disabled) {
      return false;
    }
    // disabled な option を除外して選択肢を作る
    const options = Array.from(selectEl.options).filter((opt) => !opt.disabled);
    if (options.length === 0) {
      return false;
    }
    const last = options[options.length - 1];
    if (selectEl.value === last.value) {
      return false;
    }
    selectEl.value = last.value;
    dispatchInputEvents(selectEl);
    return true;
  };
  // 非表示要素を除外して、画面上で操作できる要素だけを対象にする
  // ※ display:none / visibility:hidden / opacity:0 を弾き、サイズ0も弾く
  const isVisible = (el) => {
    const style = window.getComputedStyle(el);
    if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
      return false;
    }
    const rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  };
  // DOMが「一定時間」変化しなくなるまで待つ
  // 入力によって次の項目が表示される(DOM追加される)フォームに追従するため
  const waitForDomIdle = ({ idleMs = 120, timeoutMs = 1000 } = {}) => {
    return new Promise((resolve) => {
      let idleTimer = null;
      let timeoutTimer = null;
      let settled = false;
      const finish = (observer) => {
        if (settled) {
          return;
        }
        settled = true;
        if (idleTimer) {
          clearTimeout(idleTimer);
        }
        if (timeoutTimer) {
          clearTimeout(timeoutTimer);
        }
        observer.disconnect();
        resolve();
      };
      // DOMに変化が入るたびに idle 判定をリセットする
      const scheduleIdleFinish = (observer) => {
        if (idleTimer) {
          clearTimeout(idleTimer);
        }
        idleTimer = setTimeout(() => {
          finish(observer);
        }, idleMs);
      };
      const observer = new MutationObserver(() => {
        scheduleIdleFinish(observer);
      });
      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true
      });
      // 変化が止まらない場合でも無限待ちしないための保険
      timeoutTimer = setTimeout(() => {
        finish(observer);
      }, timeoutMs);
      scheduleIdleFinish(observer);
    });
  };
  // text/textarea に入れる連番(test1, test2...)を生成
  const createTextCounter = (start = 1) => {
    let textCounter = start;
    const next = () => {
      const v = `test${textCounter}`;
      textCounter += 1;
      return v;
    };
    const getCurrent = () => textCounter;
    return { next, getCurrent };
  };
  // 同じ要素に対しては、複数パスでも同じ文字列を使い続けるためのキャッシュ
  // WeakMap なので要素が消えれば自動で解放される
  const textValueByEl = new WeakMap();
  const getOrAssignTextValue = (el, textCounterState) => {
    const existing = textValueByEl.get(el);
    if (existing) {
      return existing;
    }
    const v = textCounterState.next();
    textValueByEl.set(el, v);
    return v;
  };
  // input[type="date"] に入れる「今日」を YYYY-MM-DD で作る
  const getTodayYmd = () => {
    const d = new Date();
    const yyyy = String(d.getFullYear());
    const mm = String(d.getMonth() + 1).padStart(2, '0');
    const dd = String(d.getDate()).padStart(2, '0');
    return `${yyyy}-${mm}-${dd}`;
  };
  // 1回分(1パス)の入力処理
  // 先に select/radio/checkbox を処理して、表示条件のトリガーになりやすい入力を先に確定させる
  // その後に text/textarea 等を埋める
  const fillInputsPass = ({ textCounterState }) => {
    let filledCount = 0;
    const inputsPhase1 = Array.from(document.querySelectorAll('input, select'))
      .filter((el) => isVisible(el));
    // radio/checkbox は name 単位でまとめて処理する
    const radioGroups = new Map();
    const checkboxGroups = new Map();
    for (const el of inputsPhase1) {
      const tag = el.tagName.toLowerCase();
      if (tag === 'select') {
        if (selectLastOption(el)) {
          filledCount += 1;
        }
        continue;
      }
      const type = (el.getAttribute('type') || 'text').toLowerCase();
      if (type === 'radio') {
        // name が無いとグループにできないのでスキップ
        if (!el.name) {
          continue;
        }
        const name = el.name;
        if (!radioGroups.has(name)) {
          radioGroups.set(name, []);
        }
        radioGroups.get(name).push(el);
        continue;
      }
      if (type === 'checkbox') {
        // name が無いとグループにできないのでスキップ
        if (!el.name) {
          continue;
        }
        const name = el.name;
        if (!checkboxGroups.has(name)) {
          checkboxGroups.set(name, []);
        }
        checkboxGroups.get(name).push(el);
        continue;
      }
    }
    // radio:同じ name の最後の要素だけON(他はOFF)
    for (const group of radioGroups.values()) {
      const last = group[group.length - 1];
      let changed = false;
      for (const el of group) {
        const ok = setChecked(el, el === last);
        if (ok) {
          changed = true;
        }
      }
      // グループ単位で1カウントにする
      if (changed) {
        filledCount += 1;
      }
    }
    // checkbox:同じ name の最後の要素だけON(他はOFF)
    for (const group of checkboxGroups.values()) {
      const last = group[group.length - 1];
      let changed = false;
      for (const el of group) {
        const ok = setChecked(el, el === last);
        if (ok) {
          changed = true;
        }
      }
      if (changed) {
        filledCount += 1;
      }
    }
    const inputsPhase2 = Array.from(document.querySelectorAll('input, textarea'))
      .filter((el) => isVisible(el));
    // date 入力用に「今日」を先に計算
    const todayYmd = getTodayYmd();
    for (const el of inputsPhase2) {
      const tag = el.tagName.toLowerCase();
      if (tag === 'textarea') {
        // textarea は連番の文字列を入れる
        const v = getOrAssignTextValue(el, textCounterState);
        if (setValue(el, v)) {
          filledCount += 1;
        }
        continue;
      }
      const type = (el.getAttribute('type') || 'text').toLowerCase();
      // radio/checkbox は Phase1 で処理済み
      if (type === 'radio' || type === 'checkbox') {
        continue;
      }
      // 対象外
      if (['button', 'submit', 'reset', 'image', 'file', 'hidden'].includes(type)) {
        continue;
      }
      // email は形式が必要なことが多いので固定値
      if (type === 'email') {
        if (setValue(el, 'test@example.com')) {
          filledCount += 1;
        }
        continue;
      }
      // date は YYYY-MM-DD を入れる(今日)
      if (type === 'date') {
        if (setValue(el, todayYmd)) {
          filledCount += 1;
        }
        continue;
      }
      // tel は電話番号形式を固定で入れる
      if (type === 'tel') {
        if (setValue(el, '09012345678')) {
          filledCount += 1;
        }
        continue;
      }
      // number は 0 を入れる
      if (type === 'number') {
        if (setValue(el, '0')) {
          filledCount += 1;
        }
        continue;
      }
      // password は 'password' を固定で入れる
      // ※ 必要なら 'Password123!' など、要件に合う文字列に変更する
      if (type === 'password') {
        if (setValue(el, 'password')) {
          filledCount += 1;
        }
        continue;
      }
      // 上記以外は連番で埋める(text/search/url など)
      const v = getOrAssignTextValue(el, textCounterState);
      if (setValue(el, v)) {
        filledCount += 1;
      }
    }
    return { filledCount };
  };
  // 複数パスで入力を実行する
  // 1パスで埋めた入力がトリガーになってDOMが増えた場合、次のパスで追加分も埋める
  // totalFilled が増えなくなったら終了
  const fillInputsSmart = async ({
    maxPass = 6,
    idleMs = 120,
    timeoutMs = 1000,
    textStart = 1
  } = {}) => {
    const textCounterState = createTextCounter(textStart);
    let totalFilled = 0;
    for (let i = 0; i < maxPass; i += 1) {
      const before = totalFilled;
      const result = fillInputsPass({ textCounterState });
      totalFilled += result.filledCount;
      // このパスで新規に埋められなければ終了
      if (totalFilled === before) {
        break;
      }
      // DOM変化が落ち着くのを待ってから次パスへ
      await waitForDomIdle({ idleMs, timeoutMs });
    }
    // filledCount: 埋めた数(目安) / nextTextNumber: 次に使う連番(デバッグ用)
    return {
      filledCount: totalFilled,
      nextTextNumber: textCounterState.getCurrent()
    };
  };
  // コンソールから再実行できるようにグローバルへ公開
  window.fillInputsSmart = fillInputsSmart;
  // 貼り付けてすぐ実行(コンソール運用前提)
  (async () => {
    const result = await window.fillInputsSmart();
    console.log('[fillInputsSmart] done:', result);
  })();
})();

カスタマイズメモ(よく変える箇所)

telをハイフン付きにする

type === 'tel'の箇所を次のように変更します。

if (type === 'tel') {
  if (setValue(el, '090-1234-5678')) {
    filledCount += 1;
  }
  continue;
}

emailの値を変える

if (type === 'email') {
  if (setValue(el, 'test@sample.com')) {
    filledCount += 1;
  }
  continue;
}

dateを固定日にする(今日ではなく任意の日付)

todayYmd の代わりに固定文字列(YYYY-MM-DD)を渡します。

if (type === 'date') {
  if (setValue(el, '2026-02-11')) {
    filledCount += 1;
  }
  continue;
}

埋めきれない場合(maxPassを増やす)

入力によりDOMが何段階も増える場合、パス回数を増やすと埋められることがあります。

window.fillInputsSmart({
  maxPass: 12,
  idleMs: 200,
  timeoutMs: 2000,
  textStart: 1
});

注意点

本番環境・個人情報入力画面で実行しない

自動入力したまま送信してしまう可能性があります。

検証環境・ローカル環境での利用を前提にしています。

対象外のtypeがある

hiddenfilebuttonsubmitなどは対象外にしています。

必要に応じて条件分岐を追加する必要があります。

-フロントエンド
-,