Skip to content

Consolidate duplicated test helpers/fixtures into shared test-helpers #157

Description

@alex-rawlings-yyc

Summary

An audit of the Jest test suite (all ~51 files under src/__tests__/) turned up
substantial duplication of fixture builders and mock scaffolding. Several helpers
that already exist in the shared files are reimplemented locally under new names,
and several fixture shapes are hand-rolled independently in 3+ files with no shared
factory. This is test-only cleanup: no product code changes, and behavior must be
unchanged (the suite should stay green with no test logic altered).

Shared helper files today:

  • src/__tests__/test-helpers.tsmakeWordToken, makePhraseLink, GEN_1_1_BOOK,
    makeStubProject, makePhraseStripContext, defaultScrRef, makeScrollGroupHook,
    makeWebViewState, createTestActivationContext
  • src/__tests__/components/test-helpers.tsxwithAnalysisStore, allFalseViewOptions

Tier 1 — Reuse an existing shared helper (mechanical, high confidence)

These reimplement a helper the suite already exports; route them through the shared one.

  • makeWordToken reimplemented under other names. Replace with the shared helper:
    • src/__tests__/utils/token-layout.test.ts mkWord (byte-for-byte identical)
    • src/__tests__/components/TokenChip.suggestions.test.tsx wordToken (only charEnd differs; never asserted)
    • src/__tests__/components/TokenChip.test.tsx + MorphemeBox.test.tsx const WORD_TOKEN (same literal in both)
    • src/__tests__/components/PhraseBox.test.tsx TEST_TOKEN / TEST_TOKEN_2 (file already imports makeWordToken)
  • STUB_PROJECT / STUB_PROJECT_2 are byte-for-byte identical between
    SelectInterlinearProjectModal.test.tsx:24 and SaveAsProjectModal.test.tsx:32.
    Promote to a single shared summary fixture (or derive from makeStubProject).
  • MOCK_FULL_PROJECT reimplements makeStubProject inline at
    ProjectModals.test.tsx:33 ({ ...MOCK_PROJECT, analysis: emptyAnalysis() } is
    makeStubProject's body). Same for its _WITH_TARGET / _WITH_SEGMENTATION variants.
    STUB_ACTIVE_PROJECT (InterlinearizerLoader.test.tsx:185) ≈ MOCK_PROJECT
    (ProjectModals.test.tsx:16).
  • makeLargeBook(count) defined twice with the same intent:
    ContinuousView.test.tsx:362 and Interlinearizer.test.tsx:303
    (differ only in token refs / textVersion). Unify.
  • defaultScrRef reinvented. GEN_1_1_SRC_REF at
    useInterlinearizerBookData.test.ts:90 equals defaultScrRef; verse-ref.test.ts
    makeRef(1,1) reproduces it. The raw { book:'GEN', chapterNum:1, verseNum:1 }
    literal is inlined 40+ times across Interlinearizer, InterlinearizerLoader,
    InterlinearNavContext, and useSegmentWindow tests — swap for defaultScrRef
    where it's the plain default.

Tier 2 — Promote a new shared helper (real duplication, needs a canonical signature)

Each bullet is a new helper to add to test-helpers.ts; pick the signature during the PR.

  • Tokenizer-driven makeBook(verses[]) appears 3× (near-identical):
    resegmentBook.test.ts:15, segmentation.test.ts:23, verse-superscripts.test.ts:16.
    makeVerseBook.
  • RawBook fixture appears 3×: makeRawBook (bookTokenizer.test.ts:14),
    the inline RawBook inside resegmentBook.test.ts's makeBook, and TEST_RAW_BOOK
    (useInterlinearizerBookData.test.ts:19). → makeRawBook (then makeVerseBook can
    be tokenizeBook(makeRawBook(...))).
  • SegmentationDispatch mock { merge, split, move: jest.fn() } duplicated in
    TokenLinkIcon.test.tsx (makeDispatch), PhraseStripParts.test.tsx (inline in
    renderBoundary), Interlinearizer.test.tsx (makeRawDispatch). → makeSegmentationDispatch.
  • Mutable scroll-group hookrenderNavMutable (InterlinearNavContext.test.tsx)
    and makeMutableScrollGroupHook/renderFadeLoader (InterlinearizerLoader.test.tsx)
    share the same let current; hook = () => [current, ()=>{}, undefined, ()=>{}].
    Consider a setRef-returning variant of makeScrollGroupHook.
  • mockUseProjectSettings — same name + near-identical body in
    useInterlinearizerBookData.test.ts and useOptimisticBooleanSetting.test.ts.
  • Punctuation-Token literalmkPunct (PhraseStripParts, TokenLinkIcon, token-layout),
    TEST_PUNCT (PhraseBox), PUNCT_TOKEN (TokenChip), plus punctuation branches of
    makeMixedBook/makeWordFreeBook/PUNCT_SEGMENT. → makePunctToken.
  • Single-/token-less GEN Segment shape built independently by verse-ref.test.ts
    makeSegment, SegmentationStore.test.tsx (inline), useSegmentWindow.test.ts
    makeSegment, and every Book builder's embedded segments. → makeSegment.
  • @papi/backend mock type-guardisPapiBackendTestMock/PapiBackendTestMock
    (main.test.ts) vs isStorageMock/StorageMock (projectStorage.test.ts): same
    __mockReadUserData/__mockWriteUserData/__mockLogger guard pattern.

Tier 3 — Widespread inlining of an existing helper (lower risk, high churn; optional)

  • withAnalysisStore bypassedAnalysisStore.test.tsx inlines
    <AnalysisStoreProvider analysisLanguage="und"> ~30×, TokenChip.test.tsx ~30×,
    PhraseBox.test.tsx renderBox reimplements it. (Note: renderChip in the
    suggestions test legitimately needs analysisLanguage="en" + extra props and can't
    use the shared wrapper as-is.)
  • makePhraseLink bypassed despite being importedSegmentView.test.tsx
    inlines the approved-link literal ~6×, ContinuousView.test.tsx ~9×.
  • allFalseViewOptions rebuilt inline in ViewOptionsDropdown.test.tsx DEFAULT_PROPS.
  • Modal beforeEach scaffoldconst mockSendCommand = jest.mocked(papi.commands.sendCommand)
    (4 files), the LOCALIZED + useLocalizedStrings → [LOCALIZED, false] triad (~8 files),
    and the defaultProps/testProps idiom recur across the modal tests. A shared
    localized-strings setup / renderModal helper would cover these.
  • MOCK_SEGMENTATION { removedVerseStarts:['GEN 1:2:0'], addedStarts:[] } inlined
    in ProjectModals, InterlinearizerLoader, main, and projectStorage tests.

Out of scope / noted but not actionable

  • requiredProps() name collides across ContinuousView / SegmentView / PhraseBox /
    TokenLinkIcon / TokenChip / ArcOverlay — same pattern, different bodies; not
    consolidatable (per-component props).
  • ResizeObserver stub harnesses (installResizeObserver in useSegmentWindow vs
    installObserverHarness in useArcPaths) share a concept but genuinely diverge.

Constraints

  • Test-only changes; no product code touched.
  • No test logic changes — only swap fixtures/builders for equivalents. Suite stays green.
  • Follow repo conventions: no as casts (no-type-assertion is enforced), full JSDoc on
    named helpers, American spelling. Run npm test and npm run lint-fix before finishing.
  • Suggest landing Tier 1 first (mechanical) as its own PR, then Tier 2, leaving Tier 3 optional.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions