ckelsoe_obsidian-plaud-impo.../jest.config.js
Charles Kelsoe 78175cf9c7 Wire up ImportModal with full state machine and error classification
Adds the ImportModal that the "Plaud Importer: Import recent recordings"
command opens. The modal walks through four states:

- loading: spinner while listRecordings is in flight
- empty: "no recordings found" when Plaud returns zero items
- error: category-specific message + Close (and Retry where it helps)
- list: newest-first checkbox list using the comfortable two-line
  layout requested for long meeting titles

main.ts constructs a single ReverseEngineeredPlaudClient in
onLayoutReady with an obsidianFetcher adapter that wraps Obsidian's
requestUrl. safeJson narrows the catch to SyntaxError only so that
non-JSON errors from the requestUrl getter are not silently swallowed.

Error classification (classifyError) routes each error class to a
distinct category with a canRetry flag and user-facing message:

- PlaudAuthError.reason === 'not_configured' → tells the user to
  open settings, no retry (modal is blocking anyway)
- PlaudAuthError.reason === 'token_rejected' → "your token was
  rejected, re-enter it", no retry
- PlaudParseError → "unexpected shape, plugin may need update", no
  retry (comes before the generic PlaudApiError branch because it
  extends PlaudApiError)
- PlaudApiError status 429 → rate-limited, retry enabled
- PlaudApiError status 500+ → server error, retry enabled
- PlaudApiError status 400-499 (not 401/429) → api-error, no retry
  ("Plaud.AI returned HTTP X, the plugin may need an update")
- PlaudApiError no status (network wrap) → network-error, retry
- Anything else → unknown, retry DISABLED and asks for bug report

Review-agent findings applied before committing:

- PlaudAuthError gained a readonly `reason` field ('not_configured'
  | 'token_rejected'). classifyError discriminates on err.reason
  instead of substring-matching on err.message, which would have
  silently misclassified the not-configured case on any future
  message rewording.
- 4xx responses other than 401/429 now map to a dedicated api-error
  category instead of being lied about as "Could not reach Plaud.AI"
  (403/404/418 would reach Plaud fine and get rejected by Plaud).
  Parametric test covers 400/403/404/418/451.
- Unknown-error fallback sets canRetry: false and asks the user to
  report the issue. Retrying an unknown error almost never helps and
  masks real bugs behind a false-hope Retry button.
- formatDuration guards against NaN/Infinity input to avoid rendering
  "NaNh NaNm NaNs" in the UI. Parser rejects these upstream today but
  the helper is exported and a future caller could slip by.
- Removed four <h2> createEl calls in favor of a single this.setTitle()
  in onOpen, which puts the title in Obsidian's modal titleEl (separate
  from contentEl) and survives state transitions automatically.
  Satisfies the Plugin guidelines rule against raw heading elements in
  plugin UI.
- refresh() now uses a monotonic fetchGeneration counter so that a
  slow in-flight fetch can't overwrite the results of a newer Retry.
- onOpen's refresh() call has a defense-in-depth .catch() in case a
  future render function throws outside the fetch try/catch.
- onImportClick stub says "NOT IMPLEMENTED" in the Notice so a future
  refactor can't accidentally ship a silent no-op Import.

Tests:

- 68 tests pass (was 60). New: 5 parametric 4xx tests, 2 not-configured
  reason-field tests (one pins that discrimination is via the enum,
  not message text), 3 parametric NaN/Infinity tests for
  formatDuration, updated class-hierarchy precedence tests.
- New __tests__/__mocks__/obsidian.ts provides an inert stub for the
  obsidian peer dep so jest can resolve imports from source files that
  use Modal/Notice/App. Wired in via jest.config.js moduleNameMapper.
- ESLint config already includes test files via the prior commit's
  jest-globals block; lint script extended to cover the new source.

styles.css replaced the placeholder with ~85 lines for the comfortable
row layout, state screens, and button row. Uses Obsidian's CSS
variables (--text-muted, --text-error, --text-accent,
--background-modifier-border, --font-monospace) instead of hardcoded
colors — compliant with Plugin guidelines.

Build clean, lint clean (0 warnings), 68/68 tests pass.
2026-04-14 09:40:10 -04:00

19 lines
596 B
JavaScript

/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
// `obsidian` is a peer dep provided at runtime by the Obsidian app —
// no real module exists for jest to resolve. Map it to an inert stub so
// source files that import from it can be loaded by the test harness.
moduleNameMapper: {
'^obsidian$': '<rootDir>/__tests__/__mocks__/obsidian.ts',
},
transform: {
'^.+\\.ts$': ['ts-jest', {
tsconfig: {
// Override ESNext module to CommonJS for Jest compatibility
module: 'CommonJS',
},
}],
},
};