Skip to content

Commit ce9970c

Browse files
authored
test: stdio client refactor for e2e (#16)
* lint, minor typing updates, multiline if * jest, separate unit and e2e configs, setupTests * e2e, stdio client refactor, migrate from custom client
1 parent aa134f6 commit ce9970c

File tree

13 files changed

+573
-326
lines changed

13 files changed

+573
-326
lines changed

eslint.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ export default [
1616
{
1717
files: ['**/*.ts'],
1818
rules: {
19+
curly: [2, 'all'],
1920
'no-unused-vars': 0,
2021
'no-undef': 0,
2122
'import/no-unresolved': 0,
2223
'jsdoc/require-returns': 0,
24+
'jsdoc/require-returns-type': 0,
2325
'jsdoc/require-param-type': 0
2426
}
2527
},
@@ -63,9 +65,14 @@ export default [
6365

6466
// Test files
6567
{
66-
files: ['**/*.test.ts'],
68+
files: [
69+
'**/*.test.ts',
70+
'tests/**/*.ts',
71+
'**/__tests__/**/*test.ts'
72+
],
6773
rules: {
6874
'@typescript-eslint/no-explicit-any': 0,
75+
'@typescript-eslint/ban-ts-comment': 1,
6976
'no-sparse-arrays': 0
7077
}
7178
}

jest.config.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
export default {
1+
const baseConfig = {
22
extensionsToTreatAsEsm: ['.ts'],
33
preset: 'ts-jest',
4-
roots: ['tests', 'src'],
54
testEnvironment: 'node',
6-
testMatch: ['<rootDir>/**/*.test.ts', '<rootDir>/src/**/*.test.ts'],
7-
setupFilesAfterEnv: ['<rootDir>/jest.setupTests.ts'],
85
testTimeout: 30000,
9-
verbose: true,
106
transform: {
117
'^.+\\.(ts|tsx)$': [
128
'ts-jest',
@@ -17,3 +13,25 @@ export default {
1713
]
1814
}
1915
};
16+
17+
export default {
18+
projects: [
19+
{
20+
displayName: 'unit',
21+
roots: ['src'],
22+
testMatch: ['<rootDir>/src/**/*.test.ts'],
23+
setupFilesAfterEnv: ['<rootDir>/jest.setupTests.ts'],
24+
...baseConfig
25+
},
26+
{
27+
displayName: 'e2e',
28+
roots: ['tests'],
29+
testMatch: ['<rootDir>/tests/**/*.test.ts'],
30+
setupFilesAfterEnv: ['<rootDir>/tests/jest.setupTests.ts'],
31+
transformIgnorePatterns: [
32+
'<rootDir>/dist/'
33+
],
34+
...baseConfig
35+
}
36+
]
37+
};

jest.setupTests.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
// Shared helpers for all Jest tests
1+
// Shared helpers for Jest unit tests
22

33
/**
44
* Note: Mock @patternfly/patternfly-component-schemas/json to avoid top-level await issues in Jest
5-
* - This package uses top-level await which Jest cannot handle without transformation.
6-
* - Individual tests can override this mock if needed
5+
* - Individual tests can override mock
76
*/
87
jest.mock('@patternfly/patternfly-component-schemas/json', () => ({
98
componentNames: ['Button', 'Alert', 'Card', 'Modal', 'AlertGroup', 'Text', 'TextInput'],
10-
getComponentSchema: jest.fn().mockImplementation((name: string) => {
11-
if (name === 'Button') {
9+
getComponentSchema: jest.fn().mockImplementation((name: unknown) => {
10+
const componentName = name as string;
11+
12+
if (componentName === 'Button') {
1213
return Promise.resolve({
1314
$schema: 'https://json-schema.org/draft/2020-12/schema',
1415
type: 'object',
@@ -24,6 +25,6 @@ jest.mock('@patternfly/patternfly-component-schemas/json', () => ({
2425
});
2526
}
2627

27-
throw new Error(`Component "${name}" not found`);
28+
throw new Error(`Component "${componentName}" not found`);
2829
})
2930
}), { virtual: true });

tests/__fixtures__/content/hosted.input.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/__fixtures__/content/local-two-files.input.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

tests/__snapshots__/mcp.test.ts.snap renamed to tests/__snapshots__/stdioTransport.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ exports[`Hosted mode, --docs-host should read llms-files and includes expected t
274274
]
275275
`;
276276

277-
exports[`PatternFly MCP should concatenate headers and separator with two local files 1`] = `
277+
exports[`PatternFly MCP, STDIO should concatenate headers and separator with two local files 1`] = `
278278
"# Documentation from documentation/guidelines/README.md
279279
280280
# PatternFly Guidelines
@@ -382,7 +382,7 @@ You can find documentation on PatternFly's components at [PatternFly All compone
382382
"
383383
`;
384384

385-
exports[`PatternFly MCP should expose expected tools and stable shape 1`] = `
385+
exports[`PatternFly MCP, STDIO should expose expected tools and stable shape 1`] = `
386386
{
387387
"toolNames": [
388388
"componentSchemas",

tests/jest.setupTests.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Shared helpers for e2e Jest tests
2+
import { jest } from '@jest/globals';
3+
4+
/**
5+
* Store the original fetch implementation
6+
* Tests can access this to get the real fetch when needed
7+
*/
8+
export const originalFetch = global.fetch;
9+
10+
/**
11+
* Set up global.fetch spy for e2e tests
12+
*
13+
* This creates a spy on global.fetch that can be overridden by individual tests.
14+
* Tests can use jest.spyOn(global, 'fetch').mockImplementation() to customize behavior.
15+
*
16+
* The spy is automatically restored after each test suite via jest.restoreAllMocks().
17+
* Individual tests should restore their mocks in afterAll/afterEach if needed.
18+
*/
19+
beforeAll(() => {
20+
jest.spyOn(global, 'fetch');
21+
});
Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
22
* Requires: npm run build prior to running Jest.
33
*/
4+
import { startServer, type StdioTransportClient } from './utils/stdioTransportClient';
5+
import { loadFixture } from './utils/fixtures';
6+
import { setupFetchMock } from './utils/fetchMock';
47

5-
import { startServer, type StdioClient } from './utils/stdioClient';
6-
import { loadFixture, startHttpFixture } from './utils/httpFixtureServer';
7-
8-
describe('PatternFly MCP', () => {
9-
let client: StdioClient;
8+
describe('PatternFly MCP, STDIO', () => {
9+
let client: StdioTransportClient;
1010

1111
beforeEach(async () => {
1212
client = await startServer();
@@ -28,25 +28,25 @@ describe('PatternFly MCP', () => {
2828
}
2929
};
3030

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

3434
expect(text.startsWith('# Documentation from')).toBe(true);
3535
expect(text).toMatchSnapshot();
3636
});
3737

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

4343
expect(toolNames).toEqual(expect.arrayContaining(['usePatternFlyDocs', 'fetchDocs']));
4444
expect({ toolNames }).toMatchSnapshot();
4545
});
4646
});
4747

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

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

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

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

8585
beforeAll(async () => {
86-
const body = loadFixture('README.md');
87-
88-
fixture = await startHttpFixture({
89-
routes: {
90-
'/readme': {
86+
// Note: The helper creates index-based paths based on routing (/0, /1, etc.), so we use /0 for the first route
87+
fetchMock = await setupFetchMock({
88+
routes: [
89+
{
90+
url: /\/readme$/,
9191
status: 200,
9292
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
93-
body
93+
body: loadFixture('README.md')
9494
}
95-
}
95+
]
9696
});
97-
url = `${fixture.baseUrl}/readme`;
97+
url = `${fetchMock.fixture.baseUrl}/0`;
9898
});
9999

100-
afterAll(async () => {
101-
await fixture.close();
102-
});
100+
afterAll(async () => fetchMock?.cleanup());
103101

104102
it('should fetch a document', async () => {
105103
const req = {

0 commit comments

Comments
 (0)