You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: connectivityProbe + Playwright e2e across Chromium/Firefox/WebKit
Round 3 audit caught two false claims in the README plus a public API
leak. Rather than rewrite the README to be more modest, make the
claims real.
connectivityProbe — captive-portal detection:
- New FormDraftOptions.connectivityProbe?: () => Promise<boolean>
- syncQueue calls it before each sync attempt. Falsy/throwing →
defer without incrementing attempt count. Retry triggers on next
online/visibilitychange event or user save().
- Read via ref so the user's probe identity can change without
recreating the queue.
- README "actual fetch determines success" rewritten as concrete
docs + a working pattern using fetch('/api/ping', { method:'HEAD' }).
Playwright e2e suite across 3 browser engines:
- playwright.config.ts with chromium / firefox / webkit projects.
- tests/e2e/headline.spec.ts: 7 headline scenarios × 3 browsers =
21 e2e tests, all green locally. Covers persist/restore, password
excludeFields, discard race, offline queue + online flush, rapid
set+discard+set, submit clears storage, multi-tab submit broadcast.
- The "iOS Safari multi-tab and offline tested" README claim is now
actually true — WebKit is the iOS Safari engine.
- CI gains a parallel e2e job with --with-deps browser install and
failure-only artifact upload.
_resetIndexedDBForTests no longer leaks into public API:
- Refactored indexedDBAdapter to hold dbPromise inside the closure
instead of at module scope. Each call yields an independent
adapter; tests get a clean slate by just calling indexedDBAdapter()
again. No more publicly-exported test escape hatch.
README factual sweep:
- "74 unit tests" → "94 unit tests + 21 Playwright e2e"
- "~3.4 KB brotli" → "~3.85 KB brotli"
- "v0.1.0-rc.0" → "v0.1.0-rc.1" (status bullet)
- "iOS Safari: multi-tab and offline tested" → concrete WebKit e2e
list
- Documented useFormDraftStatus (previously exported but absent
from docs).
- Added Captive portal section with the recommended probe pattern.
Tests: 92 → 94 unit, +21 e2e. Bundle 3.94 KB brotli (8 KB gate).
> ⚠️ **v0.1.0-rc.1 (release candidate).** Code-complete with 74 unit tests. Looking for production feedback before v0.1.0 stable. Try it, report bugs at https://github.com/mayrang/formdraft/issues.
10
+
> ⚠️ **v0.1.0-rc.2 (release candidate).** Code-complete with 94 unit tests + 21 Playwright e2e tests (Chromium / Firefox / WebKit). Looking for production feedback before v0.1.0 stable. Try it, report bugs at https://github.com/mayrang/formdraft/issues.
11
11
12
12

13
13
@@ -76,7 +76,7 @@ return <form>{/* form.register, etc. */}<span>{status}</span></form>;
76
76
| Refresh loses 20 minutes of typing | localStorage persist + mount restore |
77
77
| Server save fails silently | retry queue with exponential backoff |
78
78
| Offline write then reconnect |`online` + `visibilitychange` flush |
79
-
| Captive portal (`onLine=true` but no internet) |actual fetch determines success|
79
+
| Captive portal (`onLine=true` but no internet) |pluggable `connectivityProbe` HEAD-checks a known URL before each sync|
80
80
| Two tabs editing same draft | BroadcastChannel + `multiTab='warn'`|
81
81
| Tab A submits, Tab B keeps stale draft | submit broadcast → all tabs discard |
82
82
| Component unmounts mid-sync | guarded; no setState-on-unmounted warnings |
useFormDraft({ ..., storage: indexedDBAdapter() }); // for big forms
114
114
```
115
115
116
-
Or write your own:
116
+
## Captive portal handling
117
+
118
+
`navigator.onLine === true` lies on captive portals (hotel WiFi, etc.). Pass a probe to HEAD-check a known reachable URL before each sync:
119
+
120
+
```tsx
121
+
useFormDraft({
122
+
// ...
123
+
connectivityProbe: () =>
124
+
fetch('/api/ping', { method: 'HEAD' })
125
+
.then((r) =>r.ok)
126
+
.catch(() =>false),
127
+
});
128
+
```
129
+
130
+
When the probe returns `false`, the sync is deferred (not counted as a retry). It re-attempts on the next `online`/`visibilitychange` event, or when `save()` is called.
You don't always want to drill the `draft` object down to a deep child just to render a "Saving…" pill in a corner. Subscribe to any active `useFormDraft` instance by its `key`:
0 commit comments