最初に推奨事項をお伝えします。 私たちは Proxy Mail QA を作っているので中立ではありませんが、この問題はあなたのコードにあり、修正もコードで行います。どの製品も代わりに直してはくれません。Proxy Mail QA をおすすめするのは次の 2 つの作業です。フィールドの制限に合わせて、わざとルールに違反するテスト用パスワードを作ること。そして、何も表示しないフォームの裏で実際に何が起きたかを確かめることです。確認メールが届けば、サーバーはアカウントを作成しています。コードのバグを見つけるわけではありません。それはブラウザの DevTools や、コードを読む AI エージェントの役割です。
まず、3 つのうちどれが起きているかを調べる
ブラウザの DevTools で Network タブを開き、最小文字数より 1 文字短いなど、ルールに違反するパスワードを送信します。フィールドに合わせたパスワードを作るには、パスワード欄で Proxy Mail QA for Chrome のキーボタンを押して開くカードの 境界値パスワード リスト(「拒否されるべき」)を使うか、ルールを指定して get_password_variants を使います。メールが届くかどうか確認できるよう、新しいテスト用アドレスを使ってください。
| 見えるもの | 意味 | 調べる箇所 |
|---|---|---|
| リクエストが送信されない | ブラウザまたはコードがフォームを止めたが、何も表示しなかった | 原因 1〜4 |
| リクエストが送信され、サーバーが拒否する(400 または 422) | サーバーは役割を果たした。ページが応答を表示しなかった | 原因 5 と 6 |
| リクエストが送信され、サーバーが受け付ける | サーバーがそのパスワードを許可している。テスト用アドレスに確認メールが届く | 原因 7 |
見落とされやすいのは 3 つ目です。「エラーが出ない」フォームは、単に報告することがないだけかもしれません。サーバーのルールがページのルールより緩いためです。アプリ では、確認メールはテスト用アドレスの行に表示されます。エージェントやテストでは expect_no_email を使えます。これは何も届かなかった場合にだけ成功します。
よくある原因
フォームが何も送信していない
novalidateがあり、代わりの表示がない。 フォームやフォームライブラリがブラウザ標準のメッセージを無効にしていて(novalidate、React ではnoValidate)、独自のバリデーションがエラーを表示していません。- 入力欄にルールが設定されていない。
minlength、maxlength、requiredがないと、ブラウザはチェックするものがありません。またvalidity.tooShortはユーザーが入力した後にしか設定されないため、スクリプトで設定された値はすり抜けることがあります。 - エラーはあるが隠れている。 CSS で非表示になっている、間違った名前で表示されている(フィールドが
newPasswordなのにerrors.password)、またはフィールドが「touched」になって初めて表示されるのに、送信しても touched にならない。 - カスタムコンポーネントが入力欄を包んでいる。 フォームライブラリが実際の
<input>ではなくラッパーに接続されているため空の値をチェックしている、またはコンポーネントがエラーメッセージを渡していない。
サーバーが拒否したが、ページが何も言わなかった
- エラーが握りつぶされている。 応答に関係なく「メールを確認してください」と表示している、中身のない
catch {}を使っている、またはres.okしか処理していない。例外を投げない認証クライアントもあります。Supabase のsignUpは{ data, error }を返すため、try/catchは拒否されたパスワードを検知できません。 - ページが読み取らない形式でエラーが返ってくる。 サーバーは
{ "message": "..." }を返し、ページはerrors.passwordを探している。各サービスのエラーを、対応するフィールドに対応付けてください。
サーバーが受け付けた
- ページのルールがサーバーより厳しい。 ページは 15 文字と言い、認証サービスの設定は 6 文字を許可している。最小文字数はサービスの設定か独自のサーバーコードで設定し、ページの表示と同じにしてください。
エラーはあるが、誰にでもわかるわけではない
- 読み上げられない。 赤色だけで示されている、フィールドから離れている、スマートフォンでは画面外にある、フィールドに関連付けられていない。スクリーンリーダーの利用者には何も聞こえません。
aria-describedbyで関連付け、aria-invalid="true"を設定し、フォーカスをフィールドに移してください。
エラーを確実に表示するパターン
フレームワークに依存しないので、React、Vue など何にでも応用できます:
<form id="signup" novalidate> <label for="password">Password</label> <p id="password-rules">At least 15 characters. Any letters, numbers, symbols or spaces.</p> <input id="password" name="password" type="password" autocomplete="new-password" minlength="15" maxlength="128" required aria-describedby="password-rules password-error" /> <p id="password-error" class="error" hidden></p> <button>Create account</button></form>
<script> const form = document.getElementById('signup'); const password = form.elements.password; const error = document.getElementById('password-error');
// Count characters, not UTF-16 units, and don't rely on validity.tooShort. function rule(input) { const length = [...input.value].length; if (length === 0) return 'Enter a password.'; if (length < input.minLength) return `Use at least ${input.minLength} characters.`; if (length > input.maxLength) return `Use at most ${input.maxLength} characters.`; return ''; }
function show(message) { error.textContent = message; error.hidden = !message; password.setAttribute('aria-invalid', message ? 'true' : 'false'); }
password.addEventListener('input', () => { if (!error.hidden) show(rule(password)); });
form.addEventListener('submit', async (event) => { event.preventDefault(); const message = rule(password); if (message) { show(message); password.focus(); return; } const res = await fetch('/api/signup', { method: 'POST', body: new FormData(form) }); if (!res.ok) { const body = await res.json().catch(() => ({})); show(body.errors?.password ?? body.message ?? 'Something went wrong. Please try again.'); password.focus(); return; } location.assign('/check-your-email'); });</script>サーバーも同じルールをチェックし、拒否したパスワードには 422 と { "errors": { "password": "Use at least 15 characters." } } で応答します。ページはそれをフィールドに表示します。
maxlength についての注意:ブラウザは上限で入力を止め、貼り付けたテキストを何も言わずに切り詰めます。パスワードマネージャーが作る長さより十分大きく設定してください。そうしないと、アプリが受け取ったものより長いパスワードが保存されてしまいます。
エージェントに見つけてもらう
Qoder で Proxy Mail QA を接続している場合(Marketplace):
My sign-up form at https://staging.acme.dev/signup shows no error when the passwordbreaks the rules (15 to 128 characters). Using Proxy Mail QA, get_password_variantswith min_length 15 and max_length 128, and submit each reject value with a new testaddress (a plus tag on one address for each). For each, tell me whether a request wassent, what the server answered, what the page showed, and whether a verification emailarrived (expect_no_email).Then find the cause in the code and fix it, so the error shows at the field, isannounced to screen readers, and the server enforces the same rule as the page.修正されたことを証明する
proxymail-qa の npm パッケージを使った Playwright テストで、修正が維持されるようにします:
import { expect, test } from '@playwright/test';import { ProxyMailQA } from 'proxymail-qa';
const qa = new ProxyMailQA(); // reads PROXYMAIL_QA_API_KEY
test('a too-short password shows an error and makes no account', async ({ page }) => { const { reject } = await qa.passwordVariants({ minLength: 15, maxLength: 128 }); const tooShort = reject.filter(({ value }) => [...value].length < 15); const { address } = await qa.createAddress();
for (const [i, { tests, value }] of tooShort.entries()) { const tagged = address.replace('@', `+short${i}@`); const submitted = new Date(); await page.goto('/signup'); await page.getByLabel('Email').fill(tagged); await page.getByLabel('Password').fill(value); await page.getByRole('button', { name: 'Create account' }).click();
await expect(page.locator('#password-error'), tests).toBeVisible(); await qa.expectNoEmail(tagged, { since: submitted, timeout: 10 }); }
await qa.deleteAddress(address);});expectNoEmail は確認メールが届いた時点で例外を投げます。これはサーバーがアカウントを作成したことを意味します。プラスタグを使うと試行ごとにアプリ内で別のアカウントになり、タグ付きアドレスで待機すると、そのアドレス宛てのメールだけを見ます。
ここで Proxy Mail QA が行わないこと
- バグを見つけたり修正したりはしません。何が起きたかを示すだけで、修正はフォームとサーバーで行います。
- この 1 つのエラー以外のアクセシビリティはチェックしません。axe DevTools を実行し、スクリーンリーダーでフォームを試してください。
- ルールを決めることはしません。それについては 登録フォームのパスワードはどのくらいの長さにすべきですか? をご覧ください