import path from 'node:path' import { vi } from 'vitest' import { Minimatch } from 'minimatch' import type { FilePathAndContent } from './MockFileContentBuilder.js' import type { PathService } from '../../src/types.js' export type MockSystemFolderPath = string export type MockSystemFilePath = string export type FilePathsAndContents = FilePathAndContent[] /** * Test implementation of PathService * * Avoids the SUT from hitting file system at all, instead using a small in-memory array representing a flat filepath * list used as a pseudo directory structure. */ export default class MockPathService implements PathService { private cwd : MockSystemFolderPath private files : FilePathsAndContents getcwdMock = vi.spyOn(this, 'getcwd' as any) loadFileMock = vi.spyOn(this, 'loadFile' as any) isFileMock = vi.spyOn(this, 'isFile' as any) globMock = vi.spyOn(this, 'glob' as any) private constructor({ cwd, files } : { cwd: MockSystemFolderPath, files: FilePathsAndContents }) { this.cwd = cwd this.files = files } /** * Use the mocks if interested in low-level calls being made. * * A mock for each aspect of PathService is provided. */ getMocks() { return { getcwdMock: this.getcwdMock as unknown as Function, loadFileMock: this.loadFileMock as unknown as Function, isFileMock: this.isFileMock as unknown as Function, globMock: this.globMock as unknown as Function } } withCwd(newCwd: MockSystemFolderPath) { this.cwd = newCwd return this } withFiles(newFiles:FilePathsAndContents) { this.files = newFiles return this } static inWorkingDirectoryWithFiles({ cwd, files } : { cwd: MockSystemFolderPath, files: FilePathsAndContents }) : MockPathService { return new MockPathService({ cwd, files }) } getcwd(): MockSystemFolderPath { return this.cwd } loadFile(filePath: string): T { /** * Some test values test double slashes in paths * which is great, but will fail the simplistic exact string mock file matching, * so remove the duplication so this logic should stay simple */ const _path = path.normalize(filePath) const filePathKey = this.lookupFilesIndex(_path) const found = this.files.find(a => a[0] === filePathKey) if (found) { try { // JS's require on JS files auto-parses so let's emulate // so that test file values don't matter if they are stringed json or objects return JSON.parse(found[1] as string) as T } catch (err: any) { return found[1] as T } } throw new Error(`File "${filePathKey}" does not exist in fake file system!`) } private lookupFilesIndex(filePath: MockSystemFilePath) { const _path = path.normalize(filePath) return path.isAbsolute(_path) ? _path : path.resolve(this.cwd, _path) } isFile(filePath: MockSystemFilePath): boolean { const _path = path.normalize(filePath) const filePathKey = this.lookupFilesIndex(_path) return this.files.some(a => a[0] === filePathKey) } glob(pattern: string): string[] { const mm = new Minimatch(pattern) return this.files.filter(a => mm.match(a[0])).map(result => result[0]) } ensureAbsolutePath(filepath: string) : string { return filepath } }