Skip to content

Commit e416f37

Browse files
authored
Merge pull request #1190 from revisit-studies/jk/tests
Add more testing
2 parents 221afea + 0a14eb3 commit e416f37

162 files changed

Lines changed: 25485 additions & 2688 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ How you should interact with the codebase
3434
When working with the ReVISit codebase, work only with the source code files available to you. If you need an external library, please ask for approval first (and include how well used the library is). Make sure to follow best practices for React and TypeScript development, including proper state management, component structuring, and code documentation. Pay extra attention to lifecycle methods and hooks to ensure optimal performance and avoid memory leaks, including any updates to existing code. If you encounter any issues or have suggestions for improvements, feel free to bring them up for discussion. You can run git commands but don't run them unless asked to. Don't interact with GitHub directly. Always check package.json for the scripts available to you for building, testing, and running the project.
3535

3636
Testing
37-
When adding a new feature or modifying code try to maximize unit test coverage. Unit tests should be colocated with the files they are testing, have the same names as the file with .spec., and use the vitest framework. Apply this to both UI/react code as well as non-UI code. For UI code, we use playwright for end-to-end testing. Try to add e2e tests for any new features that involve user interaction. E2E tests are located in the tests/ directory at the root of the project. Don't run `yarn test` directly; instead, describe the tests you want to run, and I'll handle executing them. You can run unittests locally using `yarn unittest`. Preferred commands are those listed in package.json (e.g., `yarn unittest`, `yarn lint`, `yarn typecheck`, `yarn serve`, `yarn build`).
37+
When adding a new feature or modifying code try to maximize unit test coverage. Unit tests should live in a sibling `tests/` folder near the code they are testing, keep the same base name as the tested file with `.spec.`, and use the vitest framework. For example, `src/store/hooks/useReplay.ts` should be tested in `src/store/hooks/tests/useReplay.spec.tsx`. Root-level app specs should live in `src/tests/`. Apply this to both UI/react code as well as non-UI code. For UI code, we use playwright for end-to-end testing. Try to add e2e tests for any new features that involve user interaction. E2E tests are located in the `tests/` directory at the repo root. Don't run `yarn test` directly; instead, describe the tests you want to run, and I'll handle executing them. You can run unittests locally using `yarn unittest`. Preferred commands are those listed in package.json (e.g., `yarn unittest`, `yarn lint`, `yarn typecheck`, `yarn serve`, `yarn build`).
3838

3939
Parser
4040
When adding new features, sometimes it's required to update the parser and the associated types. The parser is located in src/parser/. The parser is responsible for validating and transforming the study config JSON files into a format that the application can use. When updating the parser, ensure that you also update the corresponding types in src/parser/types.ts to reflect any changes made to the study config schema. Make sure to add unit tests for any new parser functionality to ensure its correctness. Additionally, changes to the parser types will require updates to the generated JSON schema files located in src/schemas/. You can regenerate these schema files by running `yarn generate-schemas`.

LibraryDocGenerator.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
4+
import { createRequire } from 'module';
5+
import {
6+
afterEach,
7+
describe,
8+
expect,
9+
it,
10+
} from 'vitest';
11+
12+
const require = createRequire(import.meta.url);
13+
const { generateMd, generateLibraryDocs, getLibraries } = require('./libraryDocGenerator.cjs');
14+
15+
describe('libraryDocGenerator', () => {
16+
const tempDirs: string[] = [];
17+
18+
afterEach(() => {
19+
tempDirs.forEach((dir) => {
20+
if (fs.existsSync(dir)) {
21+
fs.rmSync(dir, { recursive: true, force: true });
22+
}
23+
});
24+
tempDirs.length = 0;
25+
});
26+
27+
it('generateMd includes components, sequences, and reference sections', () => {
28+
const md = generateMd('demo-lib', {
29+
description: 'Demo description',
30+
reference: 'Some reference',
31+
doi: '10.1000/xyz',
32+
externalLink: 'https://example.com',
33+
components: { beta: {}, alpha: {} },
34+
sequences: { second: {}, first: {} },
35+
additionalDescription: 'Extra details',
36+
}, true);
37+
38+
expect(md).toContain('# demo-lib');
39+
expect(md).toContain('## Available Components');
40+
expect(md).toContain('- alpha');
41+
expect(md).toContain('- beta');
42+
expect(md).toContain('## Available Sequences');
43+
expect(md).toContain('- first');
44+
expect(md).toContain('- second');
45+
expect(md).toContain('## Reference');
46+
expect(md).toContain('https://dx.doi.org/10.1000/xyz');
47+
expect(md).toContain('## Additional Description');
48+
});
49+
50+
it('generateMd handles example reference text and external-link-only docs links', () => {
51+
const exampleMd = generateMd('demo-lib', {
52+
description: 'Demo description',
53+
reference: 'Some reference',
54+
components: {},
55+
sequences: {},
56+
}, false);
57+
58+
expect(exampleMd).toContain('This is an example study of the library `demo-lib`.');
59+
expect(exampleMd).toContain('Some reference');
60+
expect(exampleMd).not.toContain(':::note[Reference]');
61+
62+
const docsMd = generateMd('demo-lib', {
63+
description: 'Demo description',
64+
externalLink: 'https://example.com',
65+
components: {},
66+
sequences: {},
67+
}, true);
68+
69+
expect(docsMd).toContain('referenceLinks={[');
70+
expect(docsMd).toContain('{name: "demo-lib", url: "https://example.com"}');
71+
expect(docsMd).not.toContain('{name: "DOI"');
72+
73+
const docsWithDoiOnly = generateMd('demo-lib', {
74+
description: 'Demo description',
75+
doi: '10.1000/xyz',
76+
components: {},
77+
sequences: {},
78+
}, true);
79+
80+
expect(docsWithDoiOnly).toContain('{name: "DOI", url: "https://dx.doi.org/10.1000/xyz"}');
81+
expect(docsWithDoiOnly).not.toContain('{name: "demo-lib", url:');
82+
});
83+
84+
it('getLibraries filters hidden entries and .DS_Store entries', () => {
85+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'lib-doc-list-'));
86+
tempDirs.push(base);
87+
const libsPath = path.join(base, 'public', 'libraries');
88+
fs.mkdirSync(libsPath, { recursive: true });
89+
fs.mkdirSync(path.join(libsPath, 'alpha'));
90+
fs.mkdirSync(path.join(libsPath, '.hidden'));
91+
fs.writeFileSync(path.join(libsPath, '.DS_Store'), '');
92+
93+
expect(getLibraries(libsPath)).toEqual(['alpha']);
94+
});
95+
96+
it('generateLibraryDocs writes docs and example markdown when assets folder exists', () => {
97+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'lib-doc-run-'));
98+
tempDirs.push(base);
99+
const libraryName = 'alpha';
100+
const librariesPath = path.join(base, 'public', 'libraries', libraryName);
101+
const exampleAssetsPath = path.join(base, 'public', `library-${libraryName}`, 'assets');
102+
103+
fs.mkdirSync(librariesPath, { recursive: true });
104+
fs.mkdirSync(exampleAssetsPath, { recursive: true });
105+
fs.writeFileSync(
106+
path.join(librariesPath, 'config.json'),
107+
JSON.stringify({
108+
description: 'Alpha description',
109+
components: { compA: {} },
110+
sequences: {},
111+
}),
112+
);
113+
114+
generateLibraryDocs(base);
115+
116+
const docsOut = path.join(base, 'docsLibraries', `${libraryName}.md`);
117+
const exampleOut = path.join(exampleAssetsPath, `${libraryName}.md`);
118+
119+
expect(fs.existsSync(docsOut)).toBe(true);
120+
expect(fs.existsSync(exampleOut)).toBe(true);
121+
expect(fs.readFileSync(docsOut, 'utf8')).toContain('# alpha');
122+
expect(fs.readFileSync(exampleOut, 'utf8')).toContain('This is an example study');
123+
});
124+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
4+
import { createRequire } from 'module';
5+
import {
6+
afterEach,
7+
describe,
8+
expect,
9+
it,
10+
vi,
11+
} from 'vitest';
12+
13+
const require = createRequire(import.meta.url);
14+
const {
15+
createExampleConfig,
16+
generateLibraryExamples,
17+
getLibraries,
18+
} = require('./libraryExampleStudyGenerator.cjs');
19+
20+
describe('libraryExampleStudyGenerator', () => {
21+
const tempDirs: string[] = [];
22+
23+
afterEach(() => {
24+
tempDirs.forEach((dir) => {
25+
if (fs.existsSync(dir)) {
26+
fs.rmSync(dir, { recursive: true, force: true });
27+
}
28+
});
29+
tempDirs.length = 0;
30+
});
31+
32+
it('createExampleConfig builds config with expected defaults', () => {
33+
const config = createExampleConfig('my-lib');
34+
35+
expect(config.studyMetadata.title).toBe('my-lib Example Study');
36+
expect(config.importedLibraries).toEqual(['my-lib']);
37+
expect(config.components.introduction.path).toBe('library-my-lib/assets/my-lib.md');
38+
expect(config.sequence.components).toEqual(['introduction']);
39+
});
40+
41+
it('getLibraries filters hidden entries and .DS_Store entries', () => {
42+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'lib-example-list-'));
43+
tempDirs.push(base);
44+
const libsPath = path.join(base, 'public', 'libraries');
45+
fs.mkdirSync(libsPath, { recursive: true });
46+
fs.mkdirSync(path.join(libsPath, 'alpha'));
47+
fs.mkdirSync(path.join(libsPath, '.hidden'));
48+
fs.writeFileSync(path.join(libsPath, '.DS_Store'), '');
49+
50+
expect(getLibraries(libsPath)).toEqual(['alpha']);
51+
});
52+
53+
it('generateLibraryExamples creates missing example study and invokes doc generation command', () => {
54+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'lib-example-run-'));
55+
tempDirs.push(base);
56+
const libraryName = 'alpha';
57+
const librariesPath = path.join(base, 'public', 'libraries', libraryName);
58+
fs.mkdirSync(librariesPath, { recursive: true });
59+
60+
const generateDocsFn = vi.fn();
61+
generateLibraryExamples(base, generateDocsFn);
62+
63+
const examplePath = path.join(base, 'public', `library-${libraryName}`);
64+
const configPath = path.join(examplePath, 'config.json');
65+
const assetsPath = path.join(examplePath, 'assets');
66+
67+
expect(fs.existsSync(examplePath)).toBe(true);
68+
expect(fs.existsSync(assetsPath)).toBe(true);
69+
expect(fs.existsSync(configPath)).toBe(true);
70+
expect(generateDocsFn).toHaveBeenCalledTimes(1);
71+
expect(generateDocsFn).toHaveBeenCalledWith(base);
72+
});
73+
74+
it('throws when doc generation fails', () => {
75+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'lib-example-error-'));
76+
tempDirs.push(base);
77+
fs.mkdirSync(path.join(base, 'public', 'libraries', 'alpha'), { recursive: true });
78+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
79+
const generateDocsFn = vi.fn(() => {
80+
throw new Error('test Error');
81+
});
82+
83+
expect(() => generateLibraryExamples(base, generateDocsFn)).toThrow('test Error');
84+
85+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error running libraryDocGenerator.cjs: Error: test Error'));
86+
errorSpy.mockRestore();
87+
});
88+
});

libraryDocGenerator.cjs

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,40 +54,50 @@ import StructuredLinks from '@site/src/components/StructuredLinks/StructuredLink
5454
/>` : ''}
5555
`;
5656

57-
const librariesPath = path.join(__dirname, './public/libraries');
58-
const docsLibrariesPath = path.join(__dirname, './docsLibraries');
59-
60-
const libraries = fs.readdirSync(librariesPath)
57+
const getLibraries = (libsPath) => fs.readdirSync(libsPath)
6158
.filter((library) => !library.startsWith('.') && !library.endsWith('.DS_Store'));
6259

63-
if (!fs.existsSync(docsLibrariesPath)) {
64-
fs.mkdirSync(docsLibrariesPath);
65-
}
60+
const generateLibraryDocs = (base) => {
61+
const librariesPath = path.join(base, 'public', 'libraries');
62+
const docsLibrariesPath = path.join(base, 'docsLibraries');
6663

67-
libraries.forEach((library) => {
68-
const libraryPath = path.join(librariesPath, library, 'config.json');
69-
const libraryConfig = JSON.parse(fs.readFileSync(libraryPath, 'utf8'));
64+
const libraries = getLibraries(librariesPath);
7065

71-
const docsMd = generateMd(library, libraryConfig, true);
72-
const exampleMd = generateMd(library, libraryConfig, false);
66+
if (!fs.existsSync(docsLibrariesPath)) {
67+
fs.mkdirSync(docsLibrariesPath);
68+
}
7369

74-
// Save to docsLibraries folder
75-
const docsLibraryPath = path.join(docsLibrariesPath, `${library}.md`);
76-
fs.writeFileSync(docsLibraryPath, docsMd);
77-
// eslint-disable-next-line no-console
78-
console.log(`Documentation saved to ${docsLibraryPath}`);
70+
libraries.forEach((library) => {
71+
const libraryPath = path.join(librariesPath, library, 'config.json');
72+
const libraryConfig = JSON.parse(fs.readFileSync(libraryPath, 'utf8'));
7973

80-
// Save to example study assets folder if assets folder exists
81-
// Add a prefix to baseMarkdown when saving to example assets
82-
const exampleAssetsPath = path.join(__dirname, 'public', `library-${library}`, 'assets');
83-
if (fs.existsSync(exampleAssetsPath)) {
84-
const exampleDocsPath = path.join(exampleAssetsPath, `${library}.md`);
85-
fs.writeFileSync(exampleDocsPath, exampleMd);
74+
const docsMd = generateMd(library, libraryConfig, true);
75+
const exampleMd = generateMd(library, libraryConfig, false);
8676

77+
// Save to docsLibraries folder
78+
const docsLibraryPath = path.join(docsLibrariesPath, `${library}.md`);
79+
fs.writeFileSync(docsLibraryPath, docsMd);
8780
// eslint-disable-next-line no-console
88-
console.log(`Documentation saved to ${exampleDocsPath}`);
89-
}
90-
});
81+
console.log(`Documentation saved to ${docsLibraryPath}`);
82+
83+
// Save to example study assets folder if assets folder exists
84+
// Add a prefix to baseMarkdown when saving to example assets
85+
const exampleAssetsPath = path.join(base, 'public', `library-${library}`, 'assets');
86+
if (fs.existsSync(exampleAssetsPath)) {
87+
const exampleDocsPath = path.join(exampleAssetsPath, `${library}.md`);
88+
fs.writeFileSync(exampleDocsPath, exampleMd);
89+
90+
// eslint-disable-next-line no-console
91+
console.log(`Documentation saved to ${exampleDocsPath}`);
92+
}
93+
});
94+
95+
// eslint-disable-next-line no-console
96+
console.log('Library documentation generated');
97+
};
98+
99+
if (require.main === module) {
100+
generateLibraryDocs(__dirname);
101+
}
91102

92-
// eslint-disable-next-line no-console
93-
console.log('Library documentation generated');
103+
module.exports = { generateMd, getLibraries, generateLibraryDocs };

0 commit comments

Comments
 (0)