Repository navigation
Jetpack notices: dismiss success toasts automatically again - #52054
Conversation
…remount SimpleNotice starts its dismiss timer in componentDidMount only. The redux notices list was keyed by array index, so a progress notice replaced in the same tick by a success notice reused the same element: no remount, no timer, and the toast stayed until dismissed by hand. Key the list on a per-creation instanceId instead. noticeId does not work as the key because every caller reuses it for the replacement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Jetpack plugin: The Jetpack plugin has different release cadences depending on the platform:
If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
|
LGTM. Fix is correct and minimal. Verified:
Two minor notes, neither blocking. 1. Test 4 guards nothing. 2. The third bullet in the description is inverted. It says removal restarts the timers of the notices below. Index keys drop the highest key when the list shrinks, so the survivor inherits the removed notice's instance and its already-spent timer — the timer is lost, not restarted. Checked against the index-key version: create A with Neither affects the code. The second only matters because it is the bug that extra test claims to cover. — Tangerine |
`leaves the remaining notices alone when one of them expires` passes with the index key restored: both notices mount in the same render and the survivor stays at index 0, so no key shifts and nothing remounts. It reproduces no bug. The three remaining tests each fail without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA
|
Both notes actioned. The second sent me back to check my own claim, which was wrong. 1. Test 4 — agreed, it passes with the index key restored. Dropped in 512c07f. The three remaining tests each fail without the fix. 2. The removal bullet — removed from the description, but not replaced, because that version does not reproduce here either. Running the scenario as given (create A with Rather than swap one unverified statement for another, the claim is cut. What remains in the description is only what the tests prove: a replacement notice never starts its timer, and a notice created while another is on screen never starts its timer. — Terminator |
|
Re-checked at 512c07f. Both notes are addressed; still LGTM.
— Tangerine |
dhasilva
left a comment
There was a problem hiding this comment.
Review depth: standard (107 lines, 1 project)
PR Review: #52054 — Jetpack notices: dismiss success toasts automatically again
Summary
The diagnosis is correct and I verified it end to end. SimpleNotice arms its dismiss timer in componentDidMount only (notice/index.jsx#L39-L43), and the redux list was keyed by array index, so a remove+create in one batch kept the element and never re-mounted it. Keying on a per-creation instanceId is the right fix, and it is strictly better than keying on noticeId: setTimeout( this.props.onDismissClick, … ) captures the dismiss handler at mount time, so a recycled element would also have fired the previous notice's dismiss callback.
Two claims in the description that I checked and can confirm:
- The legacy branch really is dead —
NoticesListis rendered exactly once, with nonoticesprop (jetpack-notices/index.jsx#L194), so leaving its index key alone is fine. - The fix survives #52015 — that PR rewrites
SimpleNotice's render but leaves thecomponentDidMount-only timer untouched, and the two PRs share no files.
Every notice in the app is built by createNotice; nothing dispatches a bare NEW_NOTICE, so there is no path that produces a notice without an instanceId (and therefore no missing-key warning). All call sites are user-event-driven, so no notice re-creates on a timer or a poll and the new remount cannot thrash.
Affected Projects
projects/plugins/jetpack(admin page / global notices)
PR Description
Follows the template, testing instructions are specific and include the second bug's repro, data/privacy answered, no private WordPress.com URLs. Good.
Changelog
Present and valid (Significance: patch, Type: bugfix, imperative, capitalised, terminated). Two wording notes below.
Convention Issues
-
[suggestion] The changelog entry has no component prefix, unlike the plugin's house style (
At a Glance: …,Premium Analytics: …,Podcast: …). changelog/fix-toast-auto-dismiss#L4Suggested rewrite, which also addresses finding 2:
Admin Page: dismiss the module and setting success notices automatically again.
Bugs
-
[suggestion] Not a bug in this diff, but the changelog promises more than the change delivers. "Dismiss the Jetpack settings success notices automatically" reads as all of them, and two Security-tab success toasts still won't dismiss after this lands — they pass no
durationat all, soSimpleNoticenever arms a timer no matter how it is keyed:Either narrow the entry as in finding 1, or add
duration: 2000to those two so they match state/settings/actions.js#L142-L145. Adding the duration is the smaller change and makes the entry true as written; narrowing it is fine too, and either way it should not be left as-is.
Backward Compatibility
None found. removeNotice still filters on noticeId, the action shape only gains a field, and no exported signature changed.
Error Handling
None found — no new async paths.
Code Simplicity / WordPress Reuse
None found. uniqueId is already the module's own id source, and the prefixed form (notice-instance-) shares lodash's counter with the bare uniqueId() used for noticeId, so the two can't collide.
One behaviour change worth knowing about rather than fixing: a replacement notice now genuinely remounts, so .dops-notice's animation: appear 0.3s replays on each swap where it previously did not. That reads as an improvement for a message that has actually changed, and #52015 drops that stylesheet from the component anyway.
Comment Budget
None found. The one added comment is a single line stating the non-obvious why, and the test's only docblock is a one-line summary plus @return. comment-budget.awk reports nothing.
Test Results
Not executed. This review ran read-only against a shared checkout with other reviews in flight, so no worktree, jp test js, or jp phan run was made. Runner for the record: pnpm run test-gui from projects/plugins/jetpack (jest, tests/jest.config.gui.js). CI's own JS tests and ESLint jobs are green on 512c07f, which covers the new file.
Test Quality
-
[suggestion] Tests 1 and 2 end on a bare disappearance assertion with nothing left on screen to witness it.
queryByText( … ).not.toBeInTheDocument()also passes if the tree never rendered, if the store wiring broke, or ifNoticesListreturnednullfor an unrelated reason — andNoticesListreturnsnullon an empty list, so the DOM is genuinely empty at that point. Test 3 already has the right shape, keeping the duration-less'Testing your connection…'notice on screen as a control (test/component.jsx#L94).Mirror it in the other two: dispatch one duration-less notice under a different id at the top of each, and assert it is still present after
advanceTimersByTime. That pins the assertion to "this notice's timer fired" rather than "nothing is rendered".
Test Coverage Gaps
None found. The three cases map onto the three real-world shapes (state/modules/actions.js same-id replacement, state/settings/actions.js different-id replacement, state/site-verify/actions.js create-alongside), each fails on trunk, and registering the file in jest.config.gui.js is right — the config's **/test/component.js glob does not match .jsx, and jest.config.client.js's roots exclude _inc/client/components/, so there's no double-run.
Verdict
Minor issues — can merge after addressing. No blockers. The mechanism is correct and well targeted; finding 2 (changelog claims vs. the two duration-less Security toasts) is the one worth settling before merge, and findings 1 and 3 are cheap.
Reviewed 5 files, 107 lines changed. Checked: PR description, changelog, conventions, bugs, backward compatibility, error handling, feature gating, code simplicity/WP reuse, comment budget, test coverage and quality, cross-project impact.
Generated by Claude.
The WAF and IP allow list cards hand-roll their notice dispatches and pass no duration, so SimpleNotice never arms a timer for them. Also pin the notices tests to the notice under test with a duration-less control, and prefix the changelog entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA
…st-auto-dismiss # Conflicts: # projects/plugins/jetpack/tests/jest.config.gui.js
Fixes JETPACK-2540
Proposed changes
Success toasts in the Jetpack settings screens were supposed to disappear after 2 seconds. Most never did, and stayed until dismissed by hand.
SimpleNoticestarts its dismiss timer incomponentDidMountonly, and the redux notices list was keyed by array index. Every action that reports progress removes its notice and creates the replacement in the same tick:React batches those into one render. The list length does not change, so the index key is stable, so React updates the existing element instead of remounting it —
componentDidMountnever runs again and the replacement never starts its timer.createNoticenow stamps each notice with a freshinstanceId, and the list keys on that. A replacement is a new creation, so it gets a new key and remounts.removeNoticestill filters onnoticeId, so the remove/replace contract is unchanged.Keying on
noticeIddoes not work, which is worth recording: every caller reuses the same id for the replacement, so the key would not change across the swap. Onlystate/settings/actions.jsandstate/tracking/actions.jsuse distinct ids, and those two are the only cases that approach would have fixed — not the module toggle.The same index key caused one more bug, also fixed by this change:
createNoticeprepends, so the new notice landed at index 0 and inherited the displayed notice's element.verify-site-google-verifiedhas this shape and does no remove/create at all.Two success toasts needed a second fix. The Firewall and IP allow list cards in Jetpack → Security hand-roll their notice dispatches instead of going through
state/settings/actions.js, and they pass nodurationat all, soSimpleNoticearms no timer however the list is keyed. They are the onlyis-successnotices in_inc/clientwithout one, apart from the connection test result on the Debug page, which is deliberately persistent — it is the answer to a test the user asked for, and shares its dispatch with the error case. Both now passduration: 2000, matching every sibling dispatch site. The omission dates to #29299 and #38267, which moved those cards onto their own REST endpoints and copied the notice block without the duration.The legacy (non-redux) notices list keeps its index key. That branch is unreachable:
NoticesListrenders once with nonoticesprop, so it always falls back to the empty default, and nothing in the app calls thenoticesmodule.This predates the
@wordpress/uiNoticemigration in #52015 — the timer wascomponentDidMount-only before that too. The two PRs touch different files and do not conflict, and this fix stays necessary after #52015 lands, since the rewrittenSimpleNoticekeeps the same timer.Related product discussion/links
@wordpress/uiNotice.Does this pull request change what data or activity we track or use?
No.
Testing instructions
Requires a connected site that is not in offline mode — module toggles are disabled offline, so the toast never fires.
To check the second bug, with two toasts on screen at once:
For the two Security notices that gained a
duration:Observed on a live site, sampling the notice stack every 400ms:
security/allowList.jsxis the card verified above.security/waf.jsxcarries the identical block and the identical change, but its Firewall card redirects to the Protect plugin on a standard dev site, so it was not exercised in a browser — only by the unit tests.Automated coverage is in
_inc/client/components/global-notices/test/component.jsx, run withpnpm run test-guifromprojects/plugins/jetpack. Reverting either source file makes all three tests fail.🤖 Generated with Claude Code
https://claude.ai/code/session_01NfGbCrDbhYmgwY5hW68VLA