Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 131 additions & 1 deletion packages/core/src/writers/single-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
createSplitModeOutput,
createSplitModeProps,
} from '../test-utils/split-modes';
import { type GeneratorDependency, OutputMockType, OutputMode } from '../types';
import {
type GeneratorDependency,
type GeneratorSchema,
OutputMockType,
OutputMode,
} from '../types';
import { writeSingleMode } from './single-mode';

describe('writeSingleMode — separated mocks import inline schemas from the target file', () => {
Expand Down Expand Up @@ -81,3 +86,128 @@ describe('writeSingleMode — separated mocks import inline schemas from the tar
);
});
});

// Regression coverage for https://github.com/orval-labs/orval/issues/3627
//
// On wide specs with `faker schemas: true`, shared-array import aggregation
// can strip `get<X>Mock()` factory imports from `mockOutput.imports`.
// split-mode, tags-mode, and split-tags-mode all recover these by scanning
// the finalized mock implementation. single-mode was the only writer missing
// this recovery — both the inline branch and the de-inlined branch.

const petSchema: GeneratorSchema = {
name: 'Pet',
model: 'export type Pet = { id: number };',
imports: [],
schema: { type: 'object', properties: { id: { type: 'integer' } } },
};

const createRecoveryProps = (target: string) => {
const baseProps = createSplitModeProps(target);
return {
...baseProps,
builder: {
...baseProps.builder,
schemas: [petSchema],
operations: {
listPets: createSplitModeOperation({
mockOutputs: [
{
type: OutputMockType.FAKER,
implementation: {
function:
'export const getPetResponseMock = () => ({ ...getPetMock() });',
handler: '',
handlerName: '',
},
imports: [],
},
],
}),
},
} as typeof baseProps.builder,
output: createSplitModeOutput(target, {
mode: OutputMode.SINGLE,
indexFiles: true,
schemas: path.join(path.dirname(target), 'model'),
mock: {
indexMockFiles: false,
generators: [{ type: OutputMockType.FAKER, schemas: true }],
},
}),
};
};

describe('writeSingleMode — recovers schema-factory imports stripped by aggregation (inline mocks)', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orval-single-mode-'));
});

afterEach(() => {
fs.removeSync(tmpDir);
});

it('recovers getPetMock() missing from mockOutput.imports', async () => {
const target = path.join(tmpDir, 'petstore.ts');
const importsMockCalls: Array<{ imports: readonly GeneratorDependency[] }> =
[];
const props = createRecoveryProps(target);

props.builder.importsMock = ({
imports,
}: {
imports: readonly GeneratorDependency[];
}) => {
importsMockCalls.push({ imports });
return '';
};

await writeSingleMode({ ...props, needSchema: false });

expect(importsMockCalls.length).toBeGreaterThan(0);
const allExportNames = importsMockCalls.flatMap((call) =>
call.imports.flatMap((dep) => dep.exports.map((entry) => entry.name)),
);
expect(allExportNames).toContain('getPetMock');
});
});

describe('writeSingleMode — recovers schema-factory imports stripped by aggregation (de-inlined mocks)', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orval-single-mode-'));
});

afterEach(() => {
fs.removeSync(tmpDir);
});

it('recovers getPetMock() in the generated .faker.ts file', async () => {
const target = path.join(tmpDir, 'petstore.ts');
const props = createRecoveryProps(target);

props.output.mock.path = path.join(tmpDir, 'mocks');
props.builder.importsMock = ({
imports,
}: {
imports: readonly GeneratorDependency[];
}) =>
imports
.map(
({ dependency, exports }: GeneratorDependency) =>
`import { ${exports.map((entry) => entry.name).join(', ')} } from '${dependency}';`,
)
.join('\n');

await writeSingleMode({ ...props, needSchema: false });

const mockContent = await fs.readFile(
path.join(tmpDir, 'mocks', 'petstore.faker.ts'),
'utf8',
);
expect(mockContent).toContain('getPetMock');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
});
75 changes: 65 additions & 10 deletions packages/core/src/writers/single-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
filterLocalStrictMockTypeImports,
} from './finalize-mock-implementation';
import { generateImportsForBuilder } from './generate-imports-for-builder';
import {
collectRecoveredSchemaFactoryImports,
mergeGeneratorImports,
} from './mock-imports';
import { collapseInlineMockOutputs } from './mock-outputs';
import {
getMockDir,
Expand Down Expand Up @@ -175,8 +179,29 @@ export async function writeSingleMode({
output,
mockOutput,
);
const finalizedMockImplementation = builder.finalizeMockImplementation
? builder.finalizeMockImplementation(
mockOutput.implementation,
finalizeMockOptions,
)
: mockOutput.implementation;
const usesSchemaFactories =
!!entry &&
!isFunction(entry) &&
entry.type === OutputMockType.FAKER &&
entry.schemas === true;
const recoveredSchemaFactoryImports =
usesSchemaFactories && output.schemas
? collectRecoveredSchemaFactoryImports(
finalizedMockImplementation,
builder.schemas.filter((s) => s.schema).map((s) => s.name),
)
: [];
const filteredMockImports = filterLocalStrictMockTypeImports(
mockOutput.imports.filter(
mergeGeneratorImports(
mockOutput.imports,
recoveredSchemaFactoryImports,
).filter(
(impMock) =>
!normalizedImports.some(
(imp) =>
Expand All @@ -198,7 +223,7 @@ export async function writeSingleMode({
'.',
);
data += builder.importsMock({
implementation: mockOutput.implementation,
implementation: finalizedMockImplementation,
imports: importsMockForBuilder,
projectName,
hasSchemaDir: !!output.schemas,
Expand Down Expand Up @@ -303,26 +328,56 @@ export async function writeSingleMode({
schemaCustomImportPath ??
resolveMockSchemasPath(mockFilePath, schemasTarget);

const finalizeMockOptions = getFinalizeMockImplementationOptions(
output,
mockOutput,
);

const finalizedMockImplementation = builder.finalizeMockImplementation
? builder.finalizeMockImplementation(
mockOutput.implementation,
finalizeMockOptions,
)
: mockOutput.implementation;

const usesSchemaFactories =
!isFunction(rawEntry) &&
rawEntry.type === OutputMockType.FAKER &&
rawEntry.schemas === true;
const recoveredSchemaFactoryImports =
usesSchemaFactories && output.schemas
? collectRecoveredSchemaFactoryImports(
finalizedMockImplementation,
builder.schemas.filter((s) => s.schema).map((s) => s.name),
)
: [];

const importsMockForBuilder =
schemasPath || mockDir !== dirname
? generateImportsForBuilder(
output,
mockOutput.imports,
filterLocalStrictMockTypeImports(
mergeGeneratorImports(
mockOutput.imports,
recoveredSchemaFactoryImports,
),
finalizeMockOptions.strictSchemaTypeNames,
),
mockRelativeSchemasPath,
)
: generateImportsForBuilder(
output,
mockOutput.imports.filter((imp) => !!imp.importPath),
filterLocalStrictMockTypeImports(
mergeGeneratorImports(
mockOutput.imports,
recoveredSchemaFactoryImports,
),
finalizeMockOptions.strictSchemaTypeNames,
).filter((imp) => !!imp.importPath),
'.',
);

let mockData = header;
const finalizedMockImplementation = builder.finalizeMockImplementation
? builder.finalizeMockImplementation(
mockOutput.implementation,
getFinalizeMockImplementationOptions(output, mockOutput),
)
: mockOutput.implementation;
mockData += builder.importsMock({
implementation: finalizedMockImplementation,
imports: importsMockForBuilder,
Expand Down