确认弹窗里的按钮写在 <form> 里,但没写 type,而 HTML 里 <button> 的默认 type 恰好就是 submit。这些 <form> 又都没有 action、没有 onSubmit,所以点一下就会走隐式提交:浏览器 GET 当前 URL、整页刷新,onClick 里刚发出去的 fetch 跟着页面一起没了。
用户看到的是"点了确定,页面闪一下,东西还在(或者根本说不清)"。
在真浏览器里跑过一遍之后,结论比我原先写的更麻烦,这里先把它订正掉:请求其实是完整发出去的,服务端收到了,只是响应回来的时候页面已经导航走、客户端拿不到。也就是说这类操作很可能确实生效了,而界面上什么都不变——不是"看着成功其实没执行",是反过来的"看着没执行其实执行了"。下面有实测数据。
涉及的 8 处(main @ 90ed95b):
复现方式:找个页面打开对应弹窗点"确定",地址栏会闪一次带 query 的整页加载。
实测(真实浏览器,Edge 153 + Playwright 1.62):本地起构建产物 + 一个模拟后端,点的是 /ongeki/rival 页"移除对手"那个按钮(:204/:207)。两个阶段只有 type 这一个变量不同:
A) <button class="btn btn-danger btn-sm">确定</button>
B) <button class="btn btn-danger btn-sm" type="button">确定</button>
|
额外整页加载 |
服务端收到 DELETE |
页面侧结果 |
| A 现状(无 type) |
1 次 |
收到了 |
274ms 时被中断:Failed to fetch |
B 只加 type="button" |
0 次 |
收到了 |
1217ms 后正常 HTTP 200 |
A 阶段的几个细节:
- 点击后地址栏变成
/ongeki/rival?(原生表单 GET 留下的 query 串),主框架导航、整页重新加载;
- 服务端收到了那个
DELETE,但日志显示客户端在响应写出之前就断开了(clientGone: true)——服务端本来要隔 1200ms 才回,而页面侧的 fetch 在 274ms 就报错了,时间点正好是导航发生的时候;
- 结果是没有成功提示、列表里的对手还在、页面看起来什么都没发生。
B 阶段同一套操作:没有导航,请求 1217ms 后正常拿到 200,对手从列表里消失。
(早前用 jsdom 做的对照——无 type 点击触发 1 次 submit、type="button" 触发 0 次——结论与这里一致,只是它观察不到"请求已经发出去"这一层。如果你们想把这两组数据做成回归用例,我可以把脚本整理成 Playwright spec 提过来。)
顺带说下跟 #25 的关系。KeychipPage.tsx:535 那个 <form> / :538 的按钮,就是 #25 里"删除机台"那一个,但根因不一样:#25 是服务端返回 500(你们当时的回复是"下次服务器更新一并修"),这里是客户端 DOM 行为。而且同样的问题在另外 7 处也有,其中 6 处跟机台无关(公告 / Passkey / OAuth / 解绑卡片 / 卡片别名 / 对手)——也就是说这不是机台专有的毛病。当初那个 ISE 的场景里会不会也叠了这条,我不确定,可能值得一起看看。
修法就是补 type="button":
- <button className="btn btn-danger btn-sm" onClick={...}>
+ <button type="button" className="btn btn-danger btn-sm" onClick={...}>
没直接开 PR,是因为这是 8 个文件、8 处同类改动,想先问一句你们希望一次改完还是自己来。
补充一点:同代码库里 ProfilePage.tsx:673、CardsPage.tsx:229、KeychipPage.tsx:686 都规规矩矩写了 type="button",所以这应该是漏了而不是有意。真想根治的话,把这类"没有输入框 + 确定/取消"的弹窗抽成一个 <ConfirmModal onConfirm>,这个模式就不会再出现。
确认弹窗里的按钮写在
<form>里,但没写type,而 HTML 里<button>的默认 type 恰好就是submit。这些<form>又都没有action、没有onSubmit,所以点一下就会走隐式提交:浏览器 GET 当前 URL、整页刷新,onClick里刚发出去的fetch跟着页面一起没了。用户看到的是"点了确定,页面闪一下,东西还在(或者根本说不清)"。
在真浏览器里跑过一遍之后,结论比我原先写的更麻烦,这里先把它订正掉:请求其实是完整发出去的,服务端收到了,只是响应回来的时候页面已经导航走、客户端拿不到。也就是说这类操作很可能确实生效了,而界面上什么都不变——不是"看着成功其实没执行",是反过来的"看着没执行其实执行了"。下面有实测数据。
涉及的 8 处(
main@90ed95b):src/pages/AnnouncementsPage.tsx:288(form)/:291删除公告src/pages/ProfilePage.tsx:685/:688移除 Passkeysrc/pages/ProfilePage.tsx:703/:706解绑 OAuthsrc/pages/CardsPage.tsx:344/:347解绑卡片src/pages/CardsPage.tsx:406/:409删除 Access Code 别名src/pages/KeychipPage.tsx:535/:538移除机台src/pages/KeychipPage.tsx:570/:573取消信任机台src/features/ongeki/OngekiRivalPage.tsx:204/:207删除对手复现方式:找个页面打开对应弹窗点"确定",地址栏会闪一次带 query 的整页加载。
实测(真实浏览器,Edge 153 + Playwright 1.62):本地起构建产物 + 一个模拟后端,点的是
/ongeki/rival页"移除对手"那个按钮(:204/:207)。两个阶段只有type这一个变量不同:Failed to fetchtype="button"HTTP 200A 阶段的几个细节:
/ongeki/rival?(原生表单 GET 留下的 query 串),主框架导航、整页重新加载;DELETE,但日志显示客户端在响应写出之前就断开了(clientGone: true)——服务端本来要隔 1200ms 才回,而页面侧的 fetch 在 274ms 就报错了,时间点正好是导航发生的时候;B 阶段同一套操作:没有导航,请求 1217ms 后正常拿到 200,对手从列表里消失。
(早前用 jsdom 做的对照——无
type点击触发 1 次 submit、type="button"触发 0 次——结论与这里一致,只是它观察不到"请求已经发出去"这一层。如果你们想把这两组数据做成回归用例,我可以把脚本整理成 Playwright spec 提过来。)顺带说下跟 #25 的关系。
KeychipPage.tsx:535那个<form>/:538的按钮,就是 #25 里"删除机台"那一个,但根因不一样:#25 是服务端返回 500(你们当时的回复是"下次服务器更新一并修"),这里是客户端 DOM 行为。而且同样的问题在另外 7 处也有,其中 6 处跟机台无关(公告 / Passkey / OAuth / 解绑卡片 / 卡片别名 / 对手)——也就是说这不是机台专有的毛病。当初那个 ISE 的场景里会不会也叠了这条,我不确定,可能值得一起看看。修法就是补
type="button":没直接开 PR,是因为这是 8 个文件、8 处同类改动,想先问一句你们希望一次改完还是自己来。
补充一点:同代码库里
ProfilePage.tsx:673、CardsPage.tsx:229、KeychipPage.tsx:686都规规矩矩写了type="button",所以这应该是漏了而不是有意。真想根治的话,把这类"没有输入框 + 确定/取消"的弹窗抽成一个<ConfirmModal onConfirm>,这个模式就不会再出现。