Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 8 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export default [
{
files: ['**/*.ts'],
rules: {
curly: [2, 'all'],
'no-unused-vars': 0,
'no-undef': 0,
'import/no-unresolved': 0,
'jsdoc/require-returns': 0,
'jsdoc/require-returns-type': 0,
'jsdoc/require-param-type': 0
}
},
Expand Down Expand Up @@ -63,9 +65,14 @@ export default [

// Test files
{
files: ['**/*.test.ts'],
files: [
'**/*.test.ts',
'tests/**/*.ts',
'**/__tests__/**/*test.ts'
],
rules: {
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/ban-ts-comment': 1,
'no-sparse-arrays': 0
}
}
Expand Down
28 changes: 23 additions & 5 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
export default {
const baseConfig = {
extensionsToTreatAsEsm: ['.ts'],
preset: 'ts-jest',
roots: ['tests', 'src'],
testEnvironment: 'node',
testMatch: ['<rootDir>/**/*.test.ts', '<rootDir>/src/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setupTests.ts'],
testTimeout: 30000,
verbose: true,
transform: {
'^.+\\.(ts|tsx)$': [
'ts-jest',
Expand All @@ -17,3 +13,25 @@ export default {
]
}
};

export default {
projects: [
{
displayName: 'unit',
roots: ['src'],
testMatch: ['<rootDir>/src/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setupTests.ts'],
...baseConfig
},
{
displayName: 'e2e',
roots: ['tests'],
testMatch: ['<rootDir>/tests/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/jest.setupTests.ts'],
transformIgnorePatterns: [
'<rootDir>/dist/'
],
...baseConfig
}
]
};
13 changes: 7 additions & 6 deletions jest.setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Shared helpers for all Jest tests
// Shared helpers for Jest unit tests

/**
* Note: Mock @patternfly/patternfly-component-schemas/json to avoid top-level await issues in Jest
* - This package uses top-level await which Jest cannot handle without transformation.
* - Individual tests can override this mock if needed
* - Individual tests can override mock
*/
jest.mock('@patternfly/patternfly-component-schemas/json', () => ({
componentNames: ['Button', 'Alert', 'Card', 'Modal', 'AlertGroup', 'Text', 'TextInput'],
getComponentSchema: jest.fn().mockImplementation((name: string) => {
if (name === 'Button') {
getComponentSchema: jest.fn().mockImplementation((name: unknown) => {
const componentName = name as string;

if (componentName === 'Button') {
return Promise.resolve({
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
Expand All @@ -24,6 +25,6 @@ jest.mock('@patternfly/patternfly-component-schemas/json', () => ({
});
}

throw new Error(`Component "${name}" not found`);
throw new Error(`Component "${componentName}" not found`);
})
}), { virtual: true });
5 changes: 0 additions & 5 deletions tests/__fixtures__/content/hosted.input.json

This file was deleted.

6 changes: 0 additions & 6 deletions tests/__fixtures__/content/local-two-files.input.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ exports[`Hosted mode, --docs-host should read llms-files and includes expected t
]
`;

exports[`PatternFly MCP should concatenate headers and separator with two local files 1`] = `
exports[`PatternFly MCP, STDIO should concatenate headers and separator with two local files 1`] = `
"# Documentation from documentation/guidelines/README.md

# PatternFly Guidelines
Expand Down Expand Up @@ -382,7 +382,7 @@ You can find documentation on PatternFly's components at [PatternFly All compone
"
`;

exports[`PatternFly MCP should expose expected tools and stable shape 1`] = `
exports[`PatternFly MCP, STDIO should expose expected tools and stable shape 1`] = `
{
"toolNames": [
"componentSchemas",
Expand Down
21 changes: 21 additions & 0 deletions tests/jest.setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Shared helpers for e2e Jest tests
import { jest } from '@jest/globals';

/**
* Store the original fetch implementation
* Tests can access this to get the real fetch when needed
*/
export const originalFetch = global.fetch;

/**
* Set up global.fetch spy for e2e tests
*
* This creates a spy on global.fetch that can be overridden by individual tests.
* Tests can use jest.spyOn(global, 'fetch').mockImplementation() to customize behavior.
*
* The spy is automatically restored after each test suite via jest.restoreAllMocks().
* Individual tests should restore their mocks in afterAll/afterEach if needed.
*/
beforeAll(() => {
jest.spyOn(global, 'fetch');
});
46 changes: 22 additions & 24 deletions tests/mcp.test.ts → tests/stdioTransport.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Requires: npm run build prior to running Jest.
*/
import { startServer, type StdioTransportClient } from './utils/stdioTransportClient';
import { loadFixture } from './utils/fixtures';
import { setupFetchMock } from './utils/fetchMock';

import { startServer, type StdioClient } from './utils/stdioClient';
import { loadFixture, startHttpFixture } from './utils/httpFixtureServer';

describe('PatternFly MCP', () => {
let client: StdioClient;
describe('PatternFly MCP, STDIO', () => {
let client: StdioTransportClient;

beforeEach(async () => {
client = await startServer();
Expand All @@ -28,25 +28,25 @@ describe('PatternFly MCP', () => {
}
};

const resp = await client.send(req);
const text = resp?.result?.content?.[0]?.text || '';
const response = await client.send(req);
const text = response?.result?.content?.[0]?.text || '';

expect(text.startsWith('# Documentation from')).toBe(true);
expect(text).toMatchSnapshot();
});

it('should expose expected tools and stable shape', async () => {
const resp = await client.send({ method: 'tools/list' });
const tools = resp?.result?.tools || [];
const toolNames = tools.map(tool => tool.name).sort();
const response = await client.send({ method: 'tools/list' });
const tools = response?.result?.tools || [];
const toolNames = tools.map((tool: any) => tool.name).sort();

expect(toolNames).toEqual(expect.arrayContaining(['usePatternFlyDocs', 'fetchDocs']));
expect({ toolNames }).toMatchSnapshot();
});
});

describe('Hosted mode, --docs-host', () => {
let client: StdioClient;
let client: StdioTransportClient;

beforeEach(async () => {
client = await startServer({ args: ['--docs-host'] });
Expand All @@ -72,9 +72,9 @@ describe('Hosted mode, --docs-host', () => {
});

describe('External URLs', () => {
let fixture: { baseUrl: string; close: () => Promise<void>; };
let fetchMock: Awaited<ReturnType<typeof setupFetchMock>> | undefined;
let url: string;
let client: StdioClient;
let client: StdioTransportClient;

beforeEach(async () => {
client = await startServer();
Expand All @@ -83,23 +83,21 @@ describe('External URLs', () => {
afterEach(async () => client.stop());

beforeAll(async () => {
const body = loadFixture('README.md');

fixture = await startHttpFixture({
routes: {
'/readme': {
// Note: The helper creates index-based paths based on routing (/0, /1, etc.), so we use /0 for the first route
fetchMock = await setupFetchMock({
routes: [
{
url: /\/readme$/,
status: 200,
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
body
body: loadFixture('README.md')
}
}
]
});
url = `${fixture.baseUrl}/readme`;
url = `${fetchMock.fixture.baseUrl}/0`;
});

afterAll(async () => {
await fixture.close();
});
afterAll(async () => fetchMock?.cleanup());

it('should fetch a document', async () => {
const req = {
Expand Down
Loading
Loading