先说我们的建议。 Proxy Mail QA 是我们做的,所以我们并不中立,但这个问题出在你的代码里,修复也在你的代码里:没有哪个产品能替你修。我们推荐用 Proxy Mail QA 做两件事:按你字段的限制生成故意违反规则的测试密码,以及弄清静默表单背后到底发生了什么,因为如果收到了验证邮件,说明你的服务器已经创建了账号。它不会找出你代码里的 bug;那是浏览器 DevTools 或读你代码的 AI 智能体的事。
首先,弄清是三种情况中的哪一种
在浏览器 DevTools 中打开 Network 标签页,然后提交一个违反规则的密码,比如比最小长度少一个字符。要获得针对你字段生成的密码,可以用 Proxy Mail QA for Chrome 在密码字段中的钥匙按钮打开的卡片上的 Edge-case password 列表(“Should be rejected”),或者带上你的规则使用 get_password_variants。用一个新的测试地址,这样你能看到是否收到邮件。
| 你看到的 | 含义 | 去查 |
|---|---|---|
| 没有发出请求 | 浏览器或你的代码拦下了表单,但什么也没显示 | 原因 1 到 4 |
| 发出了请求,服务器拒绝了(400 或 422) | 服务器尽了职责;页面没有显示结果 | 原因 5 和 6 |
| 发出了请求,服务器接受了 | 服务器允许这个密码。测试地址收到了验证邮件 | 原因 7 |
第三种是大家最容易忽略的。一个“没有报错”的表单可能根本没什么可报,因为服务器的规则比页面上的宽松。在 app 中,验证邮件会显示在该测试地址那一行。智能体或测试可以用 expect_no_email,只有在没有邮件到达时才通过。
常见原因
表单根本没发出任何东西
- 用了
novalidate却没有替代。 表单或表单库关掉了浏览器自带的提示(novalidate,或 React 中的noValidate),而你自己的校验从未渲染错误。 - 规则没写在 input 上。 没有
minlength、maxlength或required,浏览器就没东西可查。而validity.tooShort只在用户输入后才生效,所以脚本设置的值可能绕过它。 - 错误存在但被隐藏了。 CSS 把它藏了,渲染时用错了名字(字段是
newPassword却用了errors.password),或者只在字段“touched”后才显示,而提交并不会把它标记为 touched。 - 自定义组件包住了 input。 表单库连接的是外层组件而不是真正的
<input>,所以检查的是空值,或者组件从不把错误信息往下传。
服务器拒绝了,页面却没说
- 错误被吞掉了。 代码不管响应如何都显示“Check your email”,用了空的
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 在这里不做什么
- 找到或修复 bug。它告诉你发生了什么;修复在你的表单和服务器里。
- 检查这个错误之外的无障碍问题。运行 axe DevTools,并用读屏软件试一试表单。
- 选择你的规则。相关内容见我的注册表单密码应该多长?