Skip to content

fix: stop readReports rewriting the whole report list on every read (#994) - #998

Open
MOHITKOURAV01 wants to merge 4 commits into
Aditya8369:mainfrom
MOHITKOURAV01:fix/994-readreports-write-amplification
Open

fix: stop readReports rewriting the whole report list on every read (#994)#998
MOHITKOURAV01 wants to merge 4 commits into
Aditya8369:mainfrom
MOHITKOURAV01:fix/994-readreports-write-amplification

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Fixes #994.

The write that happened whether or not anything changed

const migrated = parsed.map((report) => { /* decode entities, rename statuses */ });

try {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated));   // unconditional
} catch {  }

The docstring above decodeStoredEntities says the migration "runs once, at the migration below, and the result is written back". It ran on every call and wrote on every one.

readReports() is called from the useState initialiser (line 143) and on mount, so a plain page load serialised the entire report list — base64 image data URIs, capped at 500 KB each — and wrote it straight back over the top of itself. Synchronously, on the path to first paint, into a store this component already treats as scarce: there is a 5 MB warning threshold and a pruning path fifty lines further down.

It was also a correctness problem across tabs. Two tabs open on the hub means each one rewriting storage from whatever snapshot it read, so a report submitted in tab A can be erased by tab B merely rendering.

The write now happens only when the entity decode or a status rename actually changed something.

The regression test was failing for a different reason than it looked

does not rewrite storage when nothing needs migrating was red on main, and I assumed it was catching the bug above. It was, but it would have stayed red after the fix: makeReport() defaults to status: 'Pending', which is itself a legacy value the migration renames to 'New'. The "nothing needs migrating" fixture needed migrating.

Gave that case a current status, and added one that reads three times over an already-migrated list and asserts total silence — which is the property that actually matters.

Second half: the form's controls had no names

<select value={form.category} >   {/* accessible name: "" */}
<select value={form.severity} >   {/* accessible name: "" */}
<select value={form.hashtag} >    {/* accessible name: "" */}

A screen reader announced three unnamed combo boxes in a row. CommunityHub.test.jsx failed on it —

Found multiple elements with the role "combobox" and name ""

— and had worked around it by reaching past Testing Library to document.querySelectorAll('select')[0] and [1]. Index-based DOM access in a test is usually the markup telling you something.

Every control in the form now carries an aria-label, in all three locales. The form's severity control is named "Incident severity" rather than "Severity", because the filters block already has a properly-labelled select named "Severity" and two identical names is barely better than none — there is a test asserting the names within the form are distinct.

One thing I backed out while doing this: I initially added an aria-label to the location filter input too, then removed it. That input is already inside a wrapping <label> reading "Location (Search)", and an aria-label would have overridden the visible text — a worse outcome than leaving it alone.

Test repairs that fell out of this

Four separate cases of tests asserting against markup or copy that had moved on:

  1. The form became collapsible behind a "Report Pollution" toggle, and the round-trip tests were never updated — they were querying a form that had not rendered.
  2. Category became a required field, so a submit without one is blocked by constraint validation and onSubmit never runs.
  3. The GPS tests looked for the wrong text. They queried /Use GPS for Location/i and /GPS Location attached/i, which are the inline defaults in the JSX. translation.json overrides both with "Use Current Location" and "Location attached" — what a reader actually sees. The tests were looking for strings that never reach the screen.
  4. vi.stubGlobal('navigator', { ...globalThis.navigator, geolocation }) copies nothing. navigator's properties live on its prototype, so the spread produces {} and the component was handed an empty navigator. Replaced with a defineProperty stub that restores the original descriptor in a finally.

Checks

  • npx vitest run src/components/CommunityHub.test.jsx src/components/CommunityHub.escaping.test.jsx — 22 passed (was 16, with 6 failing)
  • npx eslint on all three files — no new problems (one pre-existing exhaustive-deps warning at line 306, deliberately annotated in the source)
  • npm run build — passes

Three component suites remain red on main and are untouched by this PR: CityPollutionLeaderboard (uses jest.mock in a Vitest project), RouteForm, and SymptomReportButton. Each has its own cause and its own issue.

…ditya8369#994)

The migration in readReports wrote its result back to localStorage
unconditionally, whether or not anything had actually changed. Its own
docstring says the migration "runs once ... and the result is written
back"; it ran on every call and wrote on every one.

readReports is called from the useState initialiser as well as on mount,
so a plain page load serialised the entire report list — base64 image
data URIs included, capped at 500 KB each — and wrote it straight back
over the top of itself, synchronously, on the path to first paint. This
is a store the component already treats as scarce: there is a 5 MB
warning and a pruning path fifty lines further down.

It was a correctness problem too. Two tabs open on the hub meant each one
rewriting storage from whatever snapshot it read, so a report submitted
in one tab could be erased by the other merely rendering.

The write now happens only when the entity decode or a status rename
actually changed something.

The regression test for this was already in the tree and failing, but for
a different reason than it looked: makeReport() defaults to status
'Pending', which is itself a legacy value the migration renames, so the
"nothing needs migrating" fixture did need migrating. Gave it a current
status and added a case that reads three times over and asserts silence.

Second half: every control in the report form now has an accessible name.
Three of the four selects had no label, no aria-label and no
aria-labelledby, so a screen reader announced three unnamed combo boxes
in a row and the tests had to reach past Testing Library to
document.querySelectorAll('select')[0] and [1] to tell them apart. The
form severity control is named "Incident severity" rather than
"Severity", because the filters block already has a select named
"Severity" and two identical names is barely better than none.

Three test-side repairs fell out of this, all of them cases that were
asserting against markup or copy that had moved on:

- the report form became collapsible behind a "Report Pollution" toggle
  and the round-trip tests were never updated, so they queried a form
  that had not rendered
- category is a required field now, so a submit without one never
  reaches onSubmit
- the GPS tests looked for "Use GPS for Location" and "GPS Location
  attached", the inline JSX defaults; translation.json overrides both
  with "Use Current Location" and "Location attached", which is what a
  reader actually sees
- vi.stubGlobal('navigator', { ...globalThis.navigator, geolocation })
  copies nothing, because navigator's properties live on its prototype.
  It handed the component an empty navigator. Replaced with a
  defineProperty stub that restores the original afterwards.
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

@MOHITKOURAV01 is attempting to deploy a commit to the Aditya Mahajan's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the ECSoC26 Contributions considered under ECSoC'26 label Aug 24, 2026
@github-actions

Copy link
Copy Markdown

Thank You for Your Contribution! 🎉

Hi @MOHITKOURAV01,

Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.

Please make sure that:

  • Your code follows the project's guidelines.
  • You have linked the appropriate issue (if applicable).
  • Screenshots are added for UI/UX changes.
  • Your PR is ready for review.

The maintainer @Aditya8369 will review your PR shortly!

Happy Contributing! 🚀

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor Author

Note on the red checks.

CI is red on main itself — the latest run on 02e98f7 failed, with 18 unit tests down across 9 files plus 218 ESLint errors. So this PR opening red is inherited, not caused. To make that checkable rather than asserted:

  • Fixed here: neither CommunityHub.test.jsx nor CommunityHub.escaping.test.jsx appears in the CI failure list any more (6 failing → 22 passing).
  • Still red, untouched by this PR: RouteForm, SymptomReportButton, CityPollutionLeaderboard, verificationService, noiseTelemetryService, solarUvService, databaseService.
  • CommunityHub.jsx keeps one pre-existing react-hooks/exhaustive-deps warning at line 306, which is deliberately annotated in the source (aqiCache is a stable ref).

@Aditya8369

Copy link
Copy Markdown
Owner

@MOHITKOURAV01 conflicts

1 similar comment
@Aditya8369

Copy link
Copy Markdown
Owner

@MOHITKOURAV01 conflicts

main restructured CommunityHub.jsx in 5dbb1a0 (community report moderation
tools) — 236 insertions, including a re-indent of the report form — so both
halves of this branch had to be re-applied onto the new file rather than
merged line by line.

readReports: the conditional write-back is back, unchanged. main's copy still
serialises the whole report list and writes it over itself on every call,
including the one in the useState initialiser.

The form: all seven controls carry their id and aria-label again, at main's
indentation. The moderation controls main added are untouched.

The three translation files merged cleanly; the seven communityHub.label* keys
are present in en, hi and bn.
@MOHITKOURAV01

Copy link
Copy Markdown
Contributor Author

Merged main in to clear the conflict.

main restructured this file in 5dbb1a0 (community report moderation tools) — 236 insertions, and the report form was re-indented by two spaces along the way — so git could not line up either half of this branch against it. Rather than hand-editing conflict markers I took main's CommunityHub.jsx whole and re-applied the two changes onto it:

readReports — the conditional write-back is back, unchanged. main's copy still does

localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated));

unconditionally, on every call, including the one in the useState initialiser at line 144.

The form — all seven controls carry their id and aria-label again, at main's indentation. The moderation controls added in 5dbb1a0 are untouched.

The three translation files merged cleanly on their own; I checked the seven communityHub.label* keys are present and the JSON parses in en, hi and bn.

$ npx vitest run src/components/CommunityHub.test.jsx src/components/CommunityHub.escaping.test.jsx
Tests  22 passed (22)

$ npx vitest run
Tests  11 failed | 1594 passed (1605)

versus 17 failed / 1584 passed on main — the six CommunityHub failures currently red on main are green on this branch. npm run lint reports one warning for this file (aqiCache in a dependency array), which is on main too and predates this branch.

main restructured CommunityHub.jsx in 5dbb1a0 (community report moderation
tools) — 236 insertions, including a re-indent of the report form — so neither
half of this branch lined up against it. Both were re-applied onto main's file
rather than merged line by line.

readReports: the conditional write-back is back, unchanged.

The form: all seven controls carry their id and aria-label again, at main's
indentation. The moderation controls main added are untouched.

The diff against main is now exactly the 60 lines this branch is for.
…rite-amplification

# Conflicts:
#	src/components/CommunityHub.escaping.test.jsx
#	src/components/CommunityHub.jsx
#	src/components/CommunityHub.test.jsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26 Contributions considered under ECSoC'26

Projects

None yet

Development

Successfully merging this pull request may close these issues.

readReports() rewrites the whole report list to localStorage on every read, and the report form's selects have no accessible name

2 participants