Cross-Platform Testing Guidelines
May 25, 2026 · View on GitHub
Overview
Graph-It-Live must work seamlessly on Windows, Linux, and macOS. All tests must be platform-agnostic to ensure reliability across environments.
Core Principles
1. Never Hardcode File Paths
❌ WRONG:
const testPath = '/Users/test/project/src/index.ts'; // macOS/Linux only
const testPath = 'C:\\Users\\test\\project\\src\\index.ts'; // Windows only
✅ CORRECT:
import * as path from 'node:path';
// Use path.join for cross-platform paths
const testPath = path.join('/', 'Users', 'test', 'project', 'src', 'index.ts');
// Or use path.resolve from project root
const testPath = path.resolve(__dirname, 'fixtures', 'sample-project', 'src', 'index.ts');
2. Use String.raw for Windows Paths in Test Data
When you need to test Windows-specific paths (backslashes), use String.raw:
❌ WRONG:
'C:\\Users\\test\\project\\src\\index.ts' // Requires double escaping
✅ CORRECT:
String.raw`C:\Users\test\project\src\index.ts` // No escaping needed
3. Always Use Node.js Path Utilities
import * as path from 'node:path';
// Joining paths
const fullPath = path.join(baseDir, 'src', 'utils', 'helper.ts');
// Resolving absolute paths
const absolutePath = path.resolve('src', 'index.ts');
// Getting directory name
const dirName = path.dirname(filePath);
// Getting file extension
const ext = path.extname(filePath);
// Normalizing paths (handles ./ and ../)
const normalizedPath = path.normalize('src/../lib/index.ts');
4. Normalize Paths for Comparison
Use the project's normalizePath() utility from src/shared/path.ts:
import { normalizePath } from '@/shared/path';
// Converts Windows backslashes to forward slashes
// Lowercases drive letters (C:\ → c:/)
// Removes trailing slashes
const normalized = normalizePath('C:\\Users\\test\\project\\src\\index.ts');
// Result: 'c:/Users/test/project/src/index.ts'
5. Handle Line Endings
Different platforms use different line endings:
- Windows:
\r\n(CRLF) - macOS/Linux:
\n(LF)
For file I/O:
// Reading - normalize line endings
const content = fs.readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
// Writing - use consistent line endings (usually LF)
fs.writeFileSync(filePath, content.replace(/\r\n/g, '\n'), 'utf-8');
For tests:
// Don't compare strings with line endings directly
expect(output.replace(/\r\n/g, '\n')).toBe(expected.replace(/\r\n/g, '\n'));
6. Test Fixtures Organization
tests/
fixtures/
sample-project/
src/
index.ts
tsconfig.json
Access fixtures:
import * as path from 'node:path';
const fixturesDir = path.resolve(__dirname, '..', 'fixtures');
const sampleProject = path.join(fixturesDir, 'sample-project');
const entryFile = path.join(sampleProject, 'src', 'index.ts');
Common Pitfalls
❌ Hardcoded Separators
const path = baseDir + '/src/index.ts'; // Fails on Windows
✅ Use path.join
const path = path.join(baseDir, 'src', 'index.ts'); // Works everywhere
❌ Case-Sensitive Assumptions
// macOS/Linux are case-sensitive, Windows is not
// Don't assume case matters
if (filePath.includes('/SRC/')) { ... } // May fail
✅ Normalize and Compare
import { normalizePath } from '@/shared/path';
const normalized = normalizePath(filePath);
if (normalized.includes('/src/')) { ... } // Consistent
❌ Absolute Path Assumptions
// Don't assume paths start with /
if (filePath.startsWith('/')) { ... } // Fails on Windows (C:\...)
✅ Use path.isAbsolute
if (path.isAbsolute(filePath)) { ... } // Works everywhere
Test Helpers for Cross-Platform Compatibility
Example: Creating Test Path Helpers
// tests/helpers/paths.ts
import * as path from 'node:path';
/**
* Get cross-platform test paths
*/
export function getTestPaths() {
return {
unixStyle: path.join('/', 'Users', 'test', 'project', 'src', 'index.ts'),
windowsStyle: String.raw`C:\Users\test\project\src\index.ts`,
relative: path.join('src', 'utils', 'helper.ts'),
};
}
/**
* Create a cross-platform path from segments
*/
export function createPath(...segments: string[]): string {
return path.join(...segments);
}
Example: Reducing Nesting Depth
❌ High Cognitive Complexity:
describe('MySchema', () => {
it('should validate', () => {
validPaths.forEach(p => {
expect(() => MySchema.parse(p)).not.toThrow();
});
});
});
✅ Extracted Helper:
function expectSchemaAccepts<T>(schema: { parse: (v: T) => unknown }, value: T) {
expect(() => schema.parse(value)).not.toThrow();
}
describe('MySchema', () => {
it('should validate', () => {
validPaths.forEach(p => expectSchemaAccepts(MySchema, p));
});
});
CI/CD Considerations
Test on All Platforms
Ensure your CI runs tests on:
- Ubuntu (Linux)
- macOS
- Windows
Example GitHub Actions matrix:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [22]
runs-on: ${{ matrix.os }}
Environment Variables
Use cross-platform environment variable access:
// ✅ Cross-platform
const workspaceRoot = process.env.WORKSPACE_ROOT ?? process.cwd();
// ❌ Unix-specific
const workspaceRoot = process.env.WORKSPACE_ROOT || '/default/path';
Checklist for New Tests
- Use
path.join()orpath.resolve()for all file paths - Use
String.rawfor Windows path literals in test data - Normalize paths before comparisons with
normalizePath() - Use
path.isAbsolute()instead of checking for leading/ - Normalize line endings when comparing file content
- Extract helpers to reduce nesting depth (keep ≤ 4 levels)
- Test locally on your platform, let CI validate others
- Avoid assumptions about case sensitivity
- Use project fixtures, not hardcoded external paths
Resources
- Node.js path module documentation
- Cross-Platform Node.js Guide
- Graph-It-Live path utilities:
src/shared/path.ts - Project test patterns:
tests/analyzer/for examples
Summary
Golden Rule: If you write a path as a string literal, you're probably doing it wrong. Use Node.js path utilities for everything.