-
Notifications
You must be signed in to change notification settings - Fork 16
Subject table download #1210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
joshunrau
merged 37 commits into
DouglasNeuroInformatics:main
from
david-roper:subject-table-download
Oct 17, 2025
Merged
Subject table download #1210
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
1847144
chore: adding csv as an option to subject table
david-roper 51cc8ff
feat: add a csv option to subject table download
david-roper fe309bc
refactor: use papa parse to interpret tsv data
david-roper 27a15a2
feat: add subject id to exports
david-roper 2dc7552
feat: add long options create makeWideRows helper function for wide o…
david-roper 883f178
feat: add long cases
david-roper b421716
feat: add create long rows function
david-roper 4c0d622
feat: create parser helper function
david-roper 57b70a9
feat: change makeLongRows to for each method instead of map
david-roper 12ae11e
chore: remove console logs
david-roper 6aa18a8
test: fix tests from linter errors
david-roper 199a4a0
test: add test file
david-roper bb98e7a
test: start dl tests
david-roper 96f6f4a
test: start tests for table uplaod
david-roper 893ba0c
chore: add type to long record, disable perfectionist for long record…
david-roper 7999b71
chore: add react testing library
david-roper 7126328
chore: update vitest config
david-roper b6c29b1
chore: add zustand mock file
david-roper 8c753ff
test: add mocks for all hooks and react components
david-roper ddfe864
test: make mocks return test data
david-roper 5940ce0
chore: fix download function
david-roper 299fc8d
test: add expected call for csv in filename
david-roper b6c5bfa
test: add mock isoString date, match csv contents
david-roper b607bd8
test: create tests to confirm contents of downloads
david-roper fbfbf94
fix: remove github type workflow package
david-roper 4d6789a
chore: update test packages and lock file
david-roper 5ccf057
chore: update web test packages
david-roper a0fa764
chore: small code rabbit fixes to zustand.ts and dl test
david-roper 1766ace
test: remove unused spyon method
david-roper 8a685b8
Update apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts
david-roper c011c01
refactor: refactor mock notification and translation
david-roper cb92937
rename describe test suite
david-roper 057d54c
test: refactor to use renderhook, set records with useInstrumentRecor…
david-roper a245fe5
test: use toBasicISOString method to compare dates of output
david-roper 83b1679
test: code rabbit change for useQuery, remove unused date
david-roper f06e4d4
test: add fix test date for all date tests
david-roper 72f323b
fix: resolve linter issues and update lockfile
david-roper File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { act } from '@testing-library/react'; | ||
| import { afterEach, vi } from 'vitest'; | ||
| import * as zustand from 'zustand'; | ||
| import type { StateCreator, StoreApi, UseBoundStore } from 'zustand'; | ||
|
|
||
| const { create: zCreate, createStore: zCreateStore } = await vi.importActual<typeof zustand>('zustand'); | ||
|
|
||
| // a variable to hold reset functions for all stores declared in the app | ||
| const STORE_RESET_FUNCTIONS = new Set<() => void>(); | ||
|
|
||
| function createUncurried<T>(stateCreator: StateCreator<T>): UseBoundStore<StoreApi<T>> { | ||
| const store = zCreate(stateCreator); | ||
| const initialState = store.getInitialState(); | ||
| STORE_RESET_FUNCTIONS.add(() => { | ||
| store.setState(initialState, true); | ||
| }); | ||
| return store; | ||
| } | ||
|
|
||
| function createStoreUncurried<T>(stateCreator: StateCreator<T>): StoreApi<T> { | ||
| const store = zCreateStore(stateCreator); | ||
| const initialState = store.getInitialState(); | ||
| STORE_RESET_FUNCTIONS.add(() => { | ||
| store.setState(initialState, true); | ||
| }); | ||
| return store; | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| act(() => { | ||
| STORE_RESET_FUNCTIONS.forEach((resetFn) => { | ||
| resetFn(); | ||
| }); | ||
| STORE_RESET_FUNCTIONS.clear(); | ||
| }); | ||
| }); | ||
|
|
||
| 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 createStore<T>(stateCreator: StateCreator<T>): StoreApi<T> { | ||
| if (typeof stateCreator === 'function') { | ||
| return createStoreUncurried(stateCreator); | ||
| } | ||
| return createStoreUncurried as unknown as StoreApi<T>; | ||
| } |
138 changes: 138 additions & 0 deletions
138
apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import { toBasicISOString } from '@douglasneuroinformatics/libjs'; | ||
| import { act, renderHook } from '@testing-library/react'; | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { useInstrumentVisualization } from '../useInstrumentVisualization'; | ||
|
|
||
| const mockUseInstrument = vi.hoisted(() => | ||
| vi.fn(() => ({ | ||
| internal: { | ||
| edition: 1, | ||
| name: 'test' | ||
| } | ||
| })) | ||
| ); | ||
|
|
||
| const mockStore = { | ||
| currentGroup: { id: 'testGroupId' }, | ||
| currentUser: { username: 'testUser' } | ||
| }; | ||
|
|
||
| const mockDownloadFn = vi.fn(); | ||
|
|
||
| const mockInfoQuery = { | ||
| data: [] | ||
| }; | ||
|
|
||
| const FIXED_TEST_DATE = new Date('2025-04-30T12:00:00Z'); | ||
| const mockInstrumentRecords = { | ||
| data: [ | ||
| { | ||
| computedMeasures: {}, | ||
| data: { someValue: 'abc' }, | ||
| date: FIXED_TEST_DATE | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| vi.mock('@/hooks/useInstrument', () => ({ | ||
| useInstrument: mockUseInstrument | ||
| })); | ||
|
|
||
| vi.mock('@/store', () => ({ | ||
| useAppStore: vi.fn((selector) => selector(mockStore)) | ||
| })); | ||
|
|
||
| vi.mock('@douglasneuroinformatics/libui/hooks', () => ({ | ||
| useDownload: vi.fn(() => mockDownloadFn), | ||
| useNotificationsStore: () => ({ addNotification: vi.fn() }), | ||
| useTranslation: () => ({ t: vi.fn((key) => key) }) | ||
| })); | ||
|
|
||
| vi.mock('@/hooks/useInstrumentInfoQuery', () => ({ | ||
| useInstrumentInfoQuery: () => mockInfoQuery | ||
| })); | ||
|
|
||
| vi.mock('@/hooks/useInstrumentRecords', () => ({ | ||
| useInstrumentRecords: () => mockInstrumentRecords | ||
| })); | ||
|
|
||
| describe('useInstrumentVisualization', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe('CSV', () => { | ||
| it('Should download', () => { | ||
| const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); | ||
| const { records } = result.current; | ||
| act(() => result.current.dl('CSV')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockDownloadFn).toHaveBeenCalledTimes(1); | ||
| const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? []; | ||
| expect(filename).toContain('.csv'); | ||
| const csvContents = getContentFn(); | ||
| expect(csvContents).toMatch(`subjectId,Date,someValue\r\ntestId,${toBasicISOString(FIXED_TEST_DATE)},abc`); | ||
| }); | ||
| }); | ||
| describe('TSV', () => { | ||
| it('Should download', () => { | ||
| const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); | ||
| const { dl, records } = result.current; | ||
| act(() => dl('TSV')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockDownloadFn).toHaveBeenCalledTimes(1); | ||
| const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? []; | ||
| expect(filename).toContain('.tsv'); | ||
| const tsvContents = getContentFn(); | ||
| expect(tsvContents).toMatch(`subjectId\tDate\tsomeValue\r\ntestId\t${toBasicISOString(FIXED_TEST_DATE)}\tabc`); | ||
| }); | ||
| }); | ||
| describe('CSV Long', () => { | ||
| it('Should download', () => { | ||
| const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); | ||
| const { dl, records } = result.current; | ||
| act(() => dl('CSV Long')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockDownloadFn).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? []; | ||
| expect(filename).toContain('.csv'); | ||
| const csvLongContents = getContentFn(); | ||
| expect(csvLongContents).toMatch( | ||
| `Date,SubjectID,Value,Variable\r\n${toBasicISOString(FIXED_TEST_DATE)},testId,abc,someValue` | ||
| ); | ||
| }); | ||
| }); | ||
| describe('TSV Long', () => { | ||
| it('Should download', () => { | ||
| const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); | ||
| const { dl, records } = result.current; | ||
| act(() => dl('TSV Long')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockDownloadFn).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? []; | ||
| expect(filename).toMatch('.tsv'); | ||
| const tsvLongContents = getContentFn(); | ||
| expect(tsvLongContents).toMatch( | ||
| `Date\tSubjectID\tValue\tVariable\r\n${toBasicISOString(FIXED_TEST_DATE)}\ttestId\tabc\tsomeValue` | ||
| ); | ||
| }); | ||
| }); | ||
| describe('JSON', () => { | ||
| it('Should download', async () => { | ||
| const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } })); | ||
| const { dl, records } = result.current; | ||
| act(() => dl('JSON')); | ||
| expect(records).toBeDefined(); | ||
| expect(mockDownloadFn).toHaveBeenCalledTimes(1); | ||
|
|
||
| const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? []; | ||
| expect(filename).toMatch('.json'); | ||
| const jsonContents = await getContentFn(); | ||
| expect(jsonContents).toContain('"someValue": "abc"'); | ||
| expect(jsonContents).toContain('"subjectID": "testId"'); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.