test(e2e): full Playwright coverage for v0.2 + RHF discard fix#14
Merged
Conversation
Adds Playwright coverage for every public surface, both v0.1 features
that previously had only unit-test coverage and the v0.2 additions.
Example app:
- Hash-based router with a landing index and one page per demo
- WizardPage (existing SignupWizard, unchanged behavior)
- ConflictPage exercises both <ConflictDialog> and <ConflictResolver>
- AutoStoragePage exercises autoAdapter migration logic with a low
5,000-char threshold so the e2e can bloat past it in one click
- ExternalControlPage drives a draft via getFormDraft() from one
sibling and reads status via useFormDraftStatus from another
- HeartbeatPage runs a 1s interval probe against a route-intercepted
/__heartbeat__ endpoint
- RhfPage / FormikPage / TanstackPage one-input demos per adapter
- SessionStoragePage / IndexedDBPage exercise non-default storage
E2E specs (28 scenarios × 3 engines = 84 tests):
- tests/e2e/conflict.spec.ts: cross-tab conflict, dialog merge, Esc
dismiss, headless resolver pick-all-remote, dialog aria-modal +
focus contract
- tests/e2e/auto-storage.spec.ts: small → localStorage, bloat →
IndexedDB migration, discard clears both
- tests/e2e/external-control.spec.ts: status sibling reactivity,
external getValues / save / discard
- tests/e2e/heartbeat.spec.ts: online → offline → online transitions
via page.route() interception (context.setOffline doesn't always
intercept same-origin loopback)
- tests/e2e/adapters.spec.ts: persist+restore+discard for RHF /
Formik / TanStack
- tests/e2e/storage-adapters.spec.ts: sessionStorage and IndexedDB
persist + don't-leak
Bug fix:
- useFormDraftRHF.discard() now wraps draft.discard() to also call
form.reset(defaultValues, { keepDefaultValues: true }), mirroring
the Formik / TanStack adapters. Previously discard cleared storage
but the RHF input still showed the stale text. Worse, RHF's watch
subscription then fired on the reset and re-patched the empty
defaults back into the just-cleared draft, silently un-discarding.
- Added ignoreNextWatchRef to swallow the library-initiated reset
events on both restore and discard paths.
- New unit test locks the regression in.
Also bumps package.json to 0.2.0, updates README badges + status
section (202 unit tests + 84 Playwright e2e, 5.42 KB / 8 KB), and
removes Formik/TanStack adapter from the open contribution list.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Playwright coverage for every public surface (both pre-existing v0.1 features that had only unit-test coverage and the new v0.2 additions), and ships one real bug fix uncovered during the e2e pass.
Test deltas:
Example app
Restructured to a tiny hash router. The existing SignupWizard stays at
/(so the original 7 headline scenarios are untouched), each new feature gets its own page under#/<route>. Lazy-loaded so each route only ships what it needs.New demo pages:
#/conflictmultiTab: 'warn'+ both<ConflictDialog>and<ConflictResolver>#/auto-storageautoAdapterwith a 5,000-char threshold for fast migration#/external-controlgetFormDraftfrom a sibling +useFormDraftStatussibling reader#/heartbeatcreateHeartbeatDetectoragainst an intercepted/__heartbeat__route#/rhf#/formik#/tanstack#/session-storage#/indexeddbE2E specs (7 new files)
conflict.spec.ts(4 tests): tab-A-saves-B-sees-conflict-dialog-merges-to-correct-result, Esc keeps local, headless resolver pick-all-remote,aria-modal=true+ focus contractauto-storage.spec.ts(2 tests): small → localStorage, bloat → IndexedDB migration withonMigrationcallback; discard clears bothexternal-control.spec.ts(4 tests):useFormDraftStatusreactivity, externalgetValues/save/discardheartbeat.spec.ts(1 test): online → offline → online transitionsadapters.spec.ts(6 tests): persist+restore+discard per RHF / Formik / TanStackstorage-adapters.spec.ts(4 tests): sessionStorage + IndexedDB persist + don't-leak-to-localStorageBug found and fixed: RHF discard
The RHF adapter exposed
discard: draft.discardraw, while Formik and TanStack adapters wrapped it to also reset the underlying form. Result:discard()clears localStorage and resetsdraft.valuesto defaultsform.reset(defaults), RHF'swatchsubscription fired on the reset, calleddraft.patch(emptyDefaults), and re-persisted the empty form back into the just-cleared draft, silently undoing the discardFix: added an
ignoreNextWatchRefflag (same pattern as Formik'signoreNextFormChangeRef). Thewatchsubscriber consumes-and-clears it, skipping the patch when the reset is library-initiated (both restore and discard paths). New unit test locks the regression in.Notable test infrastructure
page.route('**/__heartbeat__', ...)toggling instead ofcontext.setOffline(true). Playwright'ssetOfflinedoesn't reliably intercept same-origin loopback connections to the dev server, so the route handler is the deterministic way to flip reachability.document.activeElementis inside the dialog. In multi-page contexts, only one tab has window focus and programmatic.focus()on a non-active tab silently no-ops in headed Chromium.README v0.2.0
Test plan
npm run typecheck— cleannpm run lint— cleannpm test— 202 passingnpx playwright test— 84 passing across Chromium / Firefox / WebKitnpm run buildclean,npm run size5.42 KB / 8 KB