Skip to content

Conversation

@david-roper
Copy link
Collaborator

@david-roper david-roper commented Oct 10, 2025

Works on issue #1206

inserts user ID and experiment date as columns in download
properly parses exportRecords with papa parse
CSV and TSV long format downloads

tests to confirm download contents in all formats completed

Summary by CodeRabbit

  • New Features

    • Instrument visualization exports now support CSV, TSV, CSV Long, TSV Long and JSON; download menu updated to expose these options.
  • Tests

    • Added comprehensive unit tests covering all export formats and a test mock that captures and resets app store state between tests.
  • Chores

    • Added testing-library dev dependencies, introduced a test path alias for imports, and removed an unused types dev dependency.

@david-roper david-roper self-assigned this Oct 10, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds CSV/TSV wide and long export formats and JSON enhancements to instrument exports; introduces a Jest-style zustand test mock with automatic resets and tests for exports; updates Vitest aliasing; adds testing-library devDependencies to the web app and removes a root devDependency. (34 words)

Changes

Cohort / File(s) Summary
Package manifests
package.json, apps/web/package.json
Removed root devDependency @types/github-script; added devDependencies @testing-library/dom@^10.4.0 and @testing-library/react@16.2.0 to apps/web/package.json.
Test runner config
apps/web/vitest.config.ts
Added import * as path from 'node:path' and a resolve.alias mapping @ -> src via path.resolve(import.meta.dirname, 'src').
Zustand test mock
apps/web/src/__mocks__/zustand.ts
New mock exporting create() and createStore() that capture initial store state, register reset functions, and run resets in afterEach wrapped in act to restore stores between tests.
Hook implementation
apps/web/src/hooks/useInstrumentVisualization.ts
Expanded export formats to CSV, CSV Long, TSV, TSV Long, and JSON; added makeWideRows, makeLongRows, parseHelper; handles date conversion, nested/object/array values, wide/long flattening, JSON pretty-printing, and export error notifications.
Hook tests
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts
New deterministic tests for CSV/TSV (wide/long) and JSON downloads; mocks stores/queries/formatters and verifies download calls, filenames/extensions, and content structure.
UI export options
apps/web/src/routes/_app/datahub/$subjectId/table.tsx
ActionDropdown export options expanded from ["TSV","JSON"] to include TSV Long, CSV, and CSV Long.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as ActionDropdown
  participant Hook as useInstrumentVisualization
  participant API as instrument/info & records
  participant Formatter as makeWideRows / makeLongRows
  participant Parser as parseHelper / JSON
  participant Download as download util
  Note right of Hook #eef6ff: export selection triggers flow

  UI->>Hook: select subjectId + format
  Hook->>API: fetch instrument info & records
  API-->>Hook: instrument + records
  alt wide format (CSV/TSV)
    Hook->>Formatter: makeWideRows(records)
    Formatter-->>Parser: rows + delimiter
  else long format (CSV Long/TSV Long)
    Hook->>Formatter: makeLongRows(records)
    Formatter-->>Parser: rows + delimiter
  else JSON
    Hook->>Parser: attach subjectID + pretty stringify
  end
  Parser->>Download: download(filename, content, mime)
  Download-->>UI: browser file saved
  alt error (missing instrument/data)
    Hook->>Hook: notify error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Expand array export #1146 — Backend array-item expansion for exportRecords (likely coordinated with this frontend addition of wide/long CSV/TSV expansion).

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The title "Subject table download" is related to the changeset but lacks specificity about the primary improvements being made. While the changes do involve subject table download functionality, the main objectives center on adding multiple export formats (CSV, TSV, CSV Long, TSV Long, JSON) with proper column insertion and long-format support. The title doesn't convey these specific enhancements, so a teammate scanning the git history wouldn't clearly understand that the PR introduces multiple export format capabilities rather than just implementing basic download functionality. Consider revising the title to be more specific about the core changes, such as "Add multiple export formats for subject table downloads" or "Support CSV and TSV long-format exports for subject tables" to clearly communicate that the PR introduces expanded export format options, not just basic download functionality.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f06e4d4 and 72f323b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/hooks/tests/useInstrumentVisualization.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint-and-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@david-roper david-roper force-pushed the subject-table-download branch from 8cc7f6f to b607bd8 Compare October 16, 2025 14:54
@david-roper david-roper marked this pull request as ready for review October 16, 2025 14:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/routes/_app/datahub/$subjectId/table.tsx (1)

18-26: Fix crash when records is empty (records[0] is undefined).

Guard before iterating or derive from keys safely.

Apply this diff:

-  const fields: { field: string; label: string }[] = [];
-  for (const subItem in records[0]) {
-    if (!subItem.startsWith('__')) {
-      fields.push({
-        field: subItem,
-        label: camelToSnakeCase(subItem).toUpperCase()
-      });
-    }
-  }
+  const fields: { field: string; label: string }[] =
+    records.length === 0
+      ? []
+      : Object.keys(records[0]!)
+          .filter((k) => !k.startsWith('__'))
+          .map((k) => ({ field: k, label: camelToSnakeCase(k).toUpperCase() }));
🧹 Nitpick comments (7)
apps/web/src/__mocks__/zustand.ts (1)

37-49: Simplify redundant typeof checks.

StateCreator is a function; the branches can be collapsed.

Apply:

-export function create<T>(stateCreator: StateCreator<T>): UseBoundStore<StoreApi<T>> {
-  if (typeof stateCreator === 'function') {
-    return createUncurried(stateCreator);
-  }
-  return createUncurried as unknown as UseBoundStore<StoreApi<T>>;
-}
+export function create<T>(stateCreator: StateCreator<T>): UseBoundStore<StoreApi<T>> {
+  return createUncurried(stateCreator);
+}
 
-export function createStore<T>(stateCreator: StateCreator<T>): StoreApi<T> {
-  if (typeof stateCreator === 'function') {
-    return createStoreUncurried(stateCreator);
-  }
-  return createStoreUncurried as unknown as StoreApi<T>;
-}
+export function createStore<T>(stateCreator: StateCreator<T>): StoreApi<T> {
+  return createStoreUncurried(stateCreator);
+}
apps/web/src/hooks/useInstrumentVisualization.ts (4)

70-84: Unify SubjectID casing across formats (wide vs long).

Long format uses SubjectID; wide uses subjectId. Standardize to SubjectID for consistency.

Apply:

-      return exportRecords.map((item) => {
-        const obj: { [key: string]: any } = { subjectId: params.subjectId };
+      return exportRecords.map((item) => {
+        const obj: { [key: string]: any } = { SubjectID: params.subjectId };
         for (const key of columnNames) {
           const val = item[key];
           if (key === '__date__') {
             obj.Date = toBasicISOString(val as Date);
             continue;
           }
           obj[key] = typeof val === 'object' ? JSON.stringify(val) : val;
         }
         return obj;
       });

152-156: Avoid map for side-effects; consider consistent ID key.

Use forEach; optionally align to SubjectID for JSON too.

Apply:

-        exportRecords.map((item) => {
-          item.subjectID = params.subjectId;
-        });
+        exportRecords.forEach((item) => {
+          (item as any).SubjectID = params.subjectId;
+        });

64-66: Sanitize timestamp in filenames (avoid ':').

Use toBasicISOString (compact ISO) to avoid OS‑unsafe chars.

Apply:

-    const baseFilename = `${currentUser!.username}_${instrument.internal.name}_${
-      instrument.internal.edition
-    }_${new Date().toISOString()}`;
+    const baseFilename = `${currentUser!.username}_${instrument.internal.name}_${
+      instrument.internal.edition
+    }_${toBasicISOString(new Date())}`;

145-151: Disambiguate long-format filenames.

Add a “-long” suffix to prevent confusion.

Apply:

-        void download(`${baseFilename}.csv`, () => {
+        void download(`${baseFilename}-long.csv`, () => {
...
-        void download(`${baseFilename}.tsv`, () => {
+        void download(`${baseFilename}-long.tsv`, () => {

Also applies to: 168-174

apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (2)

95-95: Recommended: Remove useEffect spy or clarify intent.

Spying on React.useEffect and immediately invoking it doesn't match React's actual lifecycle (effects run after render, asynchronously). If you're trying to force effects to run synchronously for testing, consider using waitFor or renderHook from @testing-library/react instead.

If using renderHook:

-  vi.spyOn(React, 'useEffect').mockImplementation((fn) => fn());
-  const { dl, records } = useInstrumentVisualization({
+  const { result } = renderHook(() => useInstrumentVisualization({
     params: { subjectId: 'testId' }
-  });
+  }));
+  const { dl, records } = result.current;

Also applies to: 111-111, 127-127, 143-143, 159-159


88-174: Optional: Expand test coverage with edge cases.

Current tests use minimal mock data (single record, simple values). Consider adding tests for:

  • Multiple records
  • Records with nested objects and arrays (tested in makeLongRows)
  • Empty records array (error handling)
  • Missing instrument (error handling)

This would increase confidence that the export logic handles real-world data correctly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f306b74 and b607bd8.

📒 Files selected for processing (6)
  • apps/web/package.json (1 hunks)
  • apps/web/src/__mocks__/zustand.ts (1 hunks)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
  • apps/web/src/hooks/useInstrumentVisualization.ts (3 hunks)
  • apps/web/src/routes/_app/datahub/$subjectId/table.tsx (1 hunks)
  • apps/web/vitest.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)
🔇 Additional comments (2)
apps/web/src/routes/_app/datahub/$subjectId/table.tsx (1)

41-41: Download options expansion looks good.

Matches hook union types and aligns with PR goals.

apps/web/vitest.config.ts (1)

8-12: Alias addition is correct.

Clean, conventional '@' -> src mapping. No issues.

Comment on lines +86 to +122
const makeLongRows = () => {
const longRecord: { [key: string]: any }[] = [];

exportRecords.forEach((item) => {
let date: Date;

Object.entries(item).forEach(([objKey, objVal]) => {
if (objKey === '__date__') {
date = objVal as Date;
return;
}

if (Array.isArray(objVal)) {
objVal.forEach((arrayItem) => {
Object.entries(arrayItem as object).forEach(([arrKey, arrItem]) => {
longRecord.push({
Date: toBasicISOString(date),
SubjectID: params.subjectId,
Variable: `${objKey}-${arrKey}`,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
Value: arrItem
});
});
});
} else {
longRecord.push({
Date: toBasicISOString(date),
SubjectID: params.subjectId,
Value: objVal,
Variable: objKey
});
}
});
});

return longRecord;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: date depends on object iteration order; can be undefined.

Don’t rely on encountering 'date' first. Read it once, then iterate the rest.

Apply:

-    const makeLongRows = () => {
-      const longRecord: { [key: string]: any }[] = [];
-
-      exportRecords.forEach((item) => {
-        let date: Date;
-
-        Object.entries(item).forEach(([objKey, objVal]) => {
-          if (objKey === '__date__') {
-            date = objVal as Date;
-            return;
-          }
-          if (Array.isArray(objVal)) {
-            objVal.forEach((arrayItem) => {
-              Object.entries(arrayItem as object).forEach(([arrKey, arrItem]) => {
-                longRecord.push({
-                  Date: toBasicISOString(date),
-                  SubjectID: params.subjectId,
-                  Variable: `${objKey}-${arrKey}`,
-                  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
-                  Value: arrItem
-                });
-              });
-            });
-          } else {
-            longRecord.push({
-              Date: toBasicISOString(date),
-              SubjectID: params.subjectId,
-              Value: objVal,
-              Variable: objKey
-            });
-          }
-        });
-      });
-
-      return longRecord;
-    };
+    const makeLongRows = () => {
+      return exportRecords.flatMap((item) => {
+        const date = (item as any).__date__ as Date;
+        const base = { Date: toBasicISOString(date), SubjectID: params.subjectId };
+        return Object.entries(item)
+          .filter(([k]) => k !== '__date__')
+          .flatMap(([objKey, objVal]) => {
+            if (Array.isArray(objVal)) {
+              return (objVal as any[]).flatMap((arrayItem) =>
+                Object.entries(arrayItem as object).map(([arrKey, arrItem]) => ({
+                  ...base,
+                  Variable: `${objKey}-${arrKey}`,
+                  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
+                  Value: arrItem
+                }))
+              );
+            }
+            return [
+              {
+                ...base,
+                Variable: objKey,
+                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
+                Value: objVal
+              }
+            ];
+          });
+      });
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const makeLongRows = () => {
const longRecord: { [key: string]: any }[] = [];
exportRecords.forEach((item) => {
let date: Date;
Object.entries(item).forEach(([objKey, objVal]) => {
if (objKey === '__date__') {
date = objVal as Date;
return;
}
if (Array.isArray(objVal)) {
objVal.forEach((arrayItem) => {
Object.entries(arrayItem as object).forEach(([arrKey, arrItem]) => {
longRecord.push({
Date: toBasicISOString(date),
SubjectID: params.subjectId,
Variable: `${objKey}-${arrKey}`,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
Value: arrItem
});
});
});
} else {
longRecord.push({
Date: toBasicISOString(date),
SubjectID: params.subjectId,
Value: objVal,
Variable: objKey
});
}
});
});
return longRecord;
};
const makeLongRows = () => {
return exportRecords.flatMap((item) => {
const date = (item as any).__date__ as Date;
const base = { Date: toBasicISOString(date), SubjectID: params.subjectId };
return Object.entries(item)
.filter(([k]) => k !== '__date__')
.flatMap(([objKey, objVal]) => {
if (Array.isArray(objVal)) {
return (objVal as any[]).flatMap((arrayItem) =>
Object.entries(arrayItem as object).map(([arrKey, arrItem]) => ({
...base,
Variable: `${objKey}-${arrKey}`,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
Value: arrItem
}))
);
}
return [
{
...base,
Variable: objKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, perfectionist/sort-objects
Value: objVal
}
];
});
});
};
🤖 Prompt for AI Agents
In apps/web/src/hooks/useInstrumentVisualization.ts around lines 86 to 122, the
code assumes '__date__' will be encountered before other keys while iterating an
object, which can leave the date undefined; first read item['__date__'] into a
local Date variable (with proper typing and a fallback/guard if missing), then
iterate the rest of the entries skipping '__date__' to build longRecord; ensure
you don't rely on iteration order and handle the case where the date is absent
by either continuing/throwing/logging as appropriate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (3)

16-22: Past critical issue resolved.

The previous review flagged __date__ using getDate(), but the code now correctly uses new Date() to return a Date object. This issue has been addressed.


28-35: Past major issue not resolved.

The previous review identified that the mock structure is inconsistent and uses wrong types. The current code still has:

  1. useAppStore returning { store: { ... } } instead of being called with a selector
  2. currentGroup as a string instead of an object with id property
  3. currentUser as a string instead of an object with username property

This mismatch will cause the hook to fail when accessing currentGroup?.id or currentUser!.username.

Also applies to: 56-58


66-74: Past major issue not resolved.

The previous review identified that mocking React internals is fragile and recommended using renderHook from @testing-library/react. The current code still mocks useState, useEffect, and useMemo, and line 72 returns a string 'setRecords' instead of a function. This prevents proper testing of state management and lifecycle behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ccf057 and a0fa764.

📒 Files selected for processing (2)
  • apps/web/src/__mocks__/zustand.ts (1 hunks)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/mocks/zustand.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)

56-64: Major: Avoid mocking React internals; use @testing-library/react utilities.

This issue was previously flagged but remains unaddressed. Mocking useState, useEffect, and useMemo directly is fragile and bypasses actual hook behavior. Line 62 returns 'setRecords' (a string) instead of a function, and the mock prevents proper state updates. This approach hides bugs related to state management and lifecycle.

Instead, use renderHook from @testing-library/react to test the hook in a realistic environment:

+import { renderHook } from '@testing-library/react';
+
 // Remove the React mocking entirely (lines 56-64)
-vi.mock('react', async (importOriginal) => {
-  const actual = await importOriginal<typeof React>();
-  return {
-    ...actual,
-    useEffect: vi.fn(),
-    useMemo: vi.fn(),
-    useState: vi.fn(() => [mockRecords, 'setRecords'])
-  };
-});

 // In each test, replace direct hook calls with renderHook:
-const { dl, records } = useInstrumentVisualization({
-  params: { subjectId: 'testId' }
-});
+const { result } = renderHook(() => useInstrumentVisualization({
+  params: { subjectId: 'testId' }
+}));

-act(() => dl('CSV'));
+act(() => result.current.dl('CSV'));
-expect(records).toBeDefined();
+expect(result.current.records).toBeDefined();

This tests actual hook behavior including state updates and effects.

🧹 Nitpick comments (2)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (2)

78-159: Consider parameterizing repetitive test cases.

The five test suites (CSV, TSV, CSV Long, TSV Long, JSON) follow an identical pattern and differ only in format names, file extensions, delimiters, and expected content. A parameterized test structure would reduce duplication and improve maintainability.

Example refactor using it.each:

describe('Download formats', () => {
  it.each([
    {
      format: 'CSV',
      extension: '.csv',
      expectedContent: 'subjectId,Date,someValue\r\ntestId,2025-04-30,abc'
    },
    {
      format: 'TSV',
      extension: '.tsv',
      expectedContent: 'subjectId\tDate\tsomeValue\r\ntestId\t2025-04-30\tabc'
    },
    {
      format: 'CSV Long',
      extension: '.csv',
      expectedContent: 'Date,SubjectID,Value,Variable\r\n2025-04-30,testId,abc,someValue'
    },
    {
      format: 'TSV Long',
      extension: '.tsv',
      expectedContent: 'Date\tSubjectID\tValue\tVariable\r\n2025-04-30\ttestId\tabc\tsomeValue'
    }
  ])('should download $format format', ({ format, extension, expectedContent }) => {
    const { dl, records } = useInstrumentVisualization({
      params: { subjectId: 'testId' }
    });
    act(() => dl(format as any));
    
    expect(records).toBeDefined();
    expect(mockUseDownload).toHaveBeenCalledTimes(1);
    
    const [filename, getContentFn] = mockUseDownload.mock.calls[0];
    expect(filename).toContain(extension);
    const contents = getContentFn();
    expect(contents).toMatch(expectedContent);
  });
  
  it('should download JSON format', async () => {
    // JSON test remains separate due to async content retrieval
    // ... existing JSON test logic
  });
});

78-159: Consider adding error path tests.

The current tests only verify successful downloads. The hook includes error handling for missing instruments and empty records (notifications at lines 57 and 60 in useInstrumentVisualization.ts), but these paths aren't tested.

Add tests for error cases:

describe('Error handling', () => {
  it('should notify when no instrument is selected', () => {
    const mockAddNotification = vi.fn();
    vi.mocked(useNotificationsStore).mockReturnValue({ 
      addNotification: mockAddNotification 
    });
    
    // Mock useInstrument to return null
    mockUseInstrument.mockReturnValueOnce(null);
    
    const { dl } = useInstrumentVisualization({
      params: { subjectId: 'testId' }
    });
    
    act(() => dl('CSV'));
    
    expect(mockAddNotification).toHaveBeenCalledWith({
      message: 'errors.noInstrumentSelected',
      type: 'error'
    });
    expect(mockUseDownload).not.toHaveBeenCalled();
  });
  
  it('should notify when records are empty', () => {
    // Mock useState to return empty records
    // ... similar test for empty records case
  });
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1766ace and c011c01.

📒 Files selected for processing (1)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (4)

1-3: Optional: Fix import ordering per ESLint rules.

Static analysis flags multiple sorting violations. Apply this diff:

-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { renderHook, act } from '@testing-library/react';
+import { act, renderHook } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
 import { useInstrumentVisualization } from '../useInstrumentVisualization';

28-33: Optional: Reorder object keys per ESLint rules.

Static analysis flags key ordering. Apply this diff:

   data: [
     {
-      date: new Date(),
       computedMeasures: {},
+      date: new Date(),
       data: { someValue: 'abc' }
     }
   ]

79-79: Optional: Use .toBe() for exact string matching.

Lines 79, 93, 107, and 121 use .toMatch() with exact strings. Since .toMatch() is for regex patterns, .toBe() is more semantically appropriate for deterministic exact matches.

Example for line 79:

-      expect(csvContents).toMatch('subjectId,Date,someValue\r\ntestId,2025-04-30,abc');
+      expect(csvContents).toBe('subjectId,Date,someValue\r\ntestId,2025-04-30,abc');

Apply similar changes to lines 93, 107, and 121.

Also applies to: 93-93, 107-107, 121-121


63-138: Optional: Consider adding error case tests.

Tests currently cover only happy paths. Consider adding tests for error scenarios:

  • No instrument selected (instrument is null)
  • No records available (records.length === 0)

This would verify that the hook properly calls notifications.addNotification() with appropriate error messages.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb92937 and 057d54c.

📒 Files selected for processing (1)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)
🪛 ESLint
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts

[error] 1-1: Expected "expect" to come before "it".

(perfectionist/sort-named-imports)


[error] 1-1: Expected "beforeEach" to come before "vi".

(perfectionist/sort-named-imports)


[error] 2-2: Expected "@testing-library/react" to come before "vitest".

(perfectionist/sort-imports)


[error] 2-2: Expected "act" to come before "renderHook".

(perfectionist/sort-named-imports)


[error] 3-3: Missed spacing between "@testing-library/react" and "../useInstrumentVisualization".

(perfectionist/sort-imports)


[error] 31-31: Expected "computedMeasures" to come before "date".

(perfectionist/sort-objects)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 057d54c and a245fe5.

📒 Files selected for processing (1)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)
🪛 ESLint
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts

[error] 1-1: Expected "expect" to come before "it".

(perfectionist/sort-named-imports)


[error] 1-1: Expected "beforeEach" to come before "vi".

(perfectionist/sort-named-imports)


[error] 2-2: Expected "@testing-library/react" to come before "vitest".

(perfectionist/sort-imports)


[error] 2-2: Expected "act" to come before "renderHook".

(perfectionist/sort-named-imports)


[error] 3-3: Missed spacing between "@testing-library/react" and "../useInstrumentVisualization".

(perfectionist/sort-imports)


[error] 4-4: Expected "@douglasneuroinformatics/libjs" (external) to come before "../useInstrumentVisualization" (parent).

(perfectionist/sort-imports)


[error] 20-20: 'mockBasicIsoString' is assigned a value but never used. Allowed unused vars must match /^_/u.

(@typescript-eslint/no-unused-vars)


[error] 32-32: Expected "computedMeasures" to come before "date".

(perfectionist/sort-objects)

🔇 Additional comments (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)

60-140: Good test coverage for all export formats.

The suite systematically tests all 5 download formats with consistent structure. Each test verifies the download is triggered, correct file extension is used, and content matches expected format. Mock cleanup in beforeEach ensures test isolation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)

1-4: Optional: Fix import ordering.

Static analysis flags several import ordering issues per the perfectionist rule. Consider running auto-fix if this matters to the project.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a245fe5 and 83b1679.

📒 Files selected for processing (1)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)
🪛 ESLint
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts

[error] 1-1: Expected "expect" to come before "it".

(perfectionist/sort-named-imports)


[error] 1-1: Expected "beforeEach" to come before "vi".

(perfectionist/sort-named-imports)


[error] 2-2: Expected "@testing-library/react" to come before "vitest".

(perfectionist/sort-imports)


[error] 2-2: Expected "act" to come before "renderHook".

(perfectionist/sort-named-imports)


[error] 3-3: Missed spacing between "@testing-library/react" and "../useInstrumentVisualization".

(perfectionist/sort-imports)


[error] 4-4: Expected "@douglasneuroinformatics/libjs" (external) to come before "../useInstrumentVisualization" (parent).

(perfectionist/sort-imports)


[error] 30-30: Expected "computedMeasures" to come before "date".

(perfectionist/sort-objects)

🔇 Additional comments (2)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (2)

36-56: LGTM on mock implementations.

All mocks are properly structured and return the correct shapes expected by the hook. The selector pattern for useAppStore and the return values for notification/translation hooks are correct.


58-138: Excellent test coverage and structure.

The test suite comprehensively covers all five download formats with proper use of renderHook and act. Each test correctly validates both filename extension and content format. The beforeEach hook ensures proper test isolation.

@david-roper david-roper requested a review from joshunrau October 17, 2025 18:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (2)

1-4: Fix import ordering per project ESLint rules.

The perfectionist/sort-imports and perfectionist/sort-named-imports rules are enabled in your project. Reorder imports alphabetically.

Apply this diff:

-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { renderHook, act } from '@testing-library/react';
+import { act, renderHook } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
 import { useInstrumentVisualization } from '../useInstrumentVisualization';
+
 import { toBasicISOString } from '@douglasneuroinformatics/libjs';

59-139: Test coverage looks solid for the happy path.

All five download formats are tested with correct assertions. The use of FIXED_TEST_DATE ensures deterministic results.

Optional: Consider adding tests for error paths (no instrument selected, no data to export) in a future PR to increase coverage of the notification logic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83b1679 and f06e4d4.

📒 Files selected for processing (1)
  • apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts (1)
apps/web/src/hooks/useInstrumentVisualization.ts (1)
  • useInstrumentVisualization (27-203)
🪛 ESLint
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts

[error] 1-1: Expected "expect" to come before "it".

(perfectionist/sort-named-imports)


[error] 1-1: Expected "beforeEach" to come before "vi".

(perfectionist/sort-named-imports)


[error] 2-2: Expected "@testing-library/react" to come before "vitest".

(perfectionist/sort-imports)


[error] 2-2: Expected "act" to come before "renderHook".

(perfectionist/sort-named-imports)


[error] 3-3: Missed spacing between "@testing-library/react" and "../useInstrumentVisualization".

(perfectionist/sort-imports)


[error] 4-4: Expected "@douglasneuroinformatics/libjs" (external) to come before "../useInstrumentVisualization" (parent).

(perfectionist/sort-imports)


[error] 31-31: Expected "computedMeasures" to come before "date".

(perfectionist/sort-objects)

@joshunrau joshunrau merged commit 50e4dc7 into DouglasNeuroInformatics:main Oct 17, 2025
2 checks passed
@david-roper david-roper deleted the subject-table-download branch November 6, 2025 19:38
@coderabbitai coderabbitai bot mentioned this pull request Nov 18, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants