Skip to content

Commit 8a377fc

Browse files
authored
Improved admin-x tests framework (#23571)
no ref - centralized vitest config and test setup for apps using admin-x-framework/shade - improved msw implementation
1 parent 75ed9d1 commit 8a377fc

File tree

16 files changed

+914
-217
lines changed

16 files changed

+914
-217
lines changed

apps/admin-x-framework/src/test/README.md

Lines changed: 319 additions & 77 deletions
Large diffs are not rendered by default.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import {HttpResponse, http} from 'msw';
2+
import {setupServer} from 'msw/node';
3+
4+
/**
5+
* Common Ghost API response fixtures
6+
*/
7+
export const fixtures = {
8+
// Global data fixtures
9+
config: {
10+
version: '5.x',
11+
environment: 'testing',
12+
database: 'mysql8',
13+
mail: 'SMTP',
14+
labs: {},
15+
clientExtensions: {},
16+
enableDeveloperExperiments: true,
17+
stripeDirect: false
18+
},
19+
20+
settings: {
21+
title: 'Test Site',
22+
description: 'A test Ghost site',
23+
logo: null,
24+
cover_image: null,
25+
icon: null,
26+
accent_color: '#FF1A75',
27+
locale: 'en',
28+
timezone: 'Etc/UTC',
29+
codeinjection_head: null,
30+
codeinjection_foot: null,
31+
navigation: [],
32+
secondary_navigation: []
33+
},
34+
35+
site: {
36+
title: 'Test Site',
37+
url: 'http://localhost:3000',
38+
version: '5.x'
39+
},
40+
41+
user: {
42+
id: '1',
43+
name: 'Test User',
44+
slug: 'test-user',
45+
46+
profile_image: null,
47+
cover_image: null,
48+
bio: null,
49+
website: null,
50+
location: null,
51+
facebook: null,
52+
twitter: null,
53+
accessibility: null,
54+
status: 'active',
55+
meta_title: null,
56+
meta_description: null,
57+
tour: null,
58+
last_seen: '2024-01-01T00:00:00.000Z',
59+
created_at: '2024-01-01T00:00:00.000Z',
60+
updated_at: '2024-01-01T00:00:00.000Z',
61+
roles: [{
62+
id: '1',
63+
name: 'Administrator',
64+
description: 'Administrators',
65+
created_at: '2024-01-01T00:00:00.000Z',
66+
updated_at: '2024-01-01T00:00:00.000Z'
67+
}]
68+
},
69+
70+
// Links fixtures
71+
linksBulkSuccess: {
72+
bulk: {
73+
action: 'updateLink',
74+
meta: {
75+
stats: {successful: 1, unsuccessful: 0},
76+
errors: [],
77+
unsuccessfulData: []
78+
}
79+
}
80+
},
81+
82+
// Stats fixtures (from our previous work)
83+
memberCountHistory: {
84+
stats: [
85+
{date: '2024-01-01', paid: 100, free: 500, comped: 10, paid_subscribed: 5, paid_canceled: 2},
86+
{date: '2024-01-02', paid: 102, free: 505, comped: 10, paid_subscribed: 3, paid_canceled: 1}
87+
],
88+
meta: {totals: {paid: 102, free: 505, comped: 10}}
89+
},
90+
91+
mrrHistory: {
92+
stats: [
93+
{date: '2024-01-01', mrr: 1000, currency: 'USD'},
94+
{date: '2024-01-02', mrr: 1020, currency: 'USD'}
95+
],
96+
meta: {totals: {mrr: 1020}}
97+
}
98+
};
99+
100+
/**
101+
* Common Ghost API handlers
102+
*/
103+
export const handlers = {
104+
// Global data handlers
105+
browseConfig: http.get('/ghost/api/admin/config/', () => {
106+
return HttpResponse.json(fixtures.config);
107+
}),
108+
109+
browseSettings: http.get('/ghost/api/admin/settings/', () => {
110+
return HttpResponse.json({settings: [fixtures.settings]});
111+
}),
112+
113+
browseSite: http.get('/ghost/api/admin/site/', () => {
114+
return HttpResponse.json({site: fixtures.site});
115+
}),
116+
117+
browseMe: http.get('/ghost/api/admin/users/me/', () => {
118+
return HttpResponse.json({users: [fixtures.user]});
119+
}),
120+
121+
// Links handlers
122+
updateLinksBulk: http.put('/ghost/api/admin/links/bulk/', () => {
123+
return HttpResponse.json(fixtures.linksBulkSuccess);
124+
}),
125+
126+
// Stats handlers
127+
browseMemberCountHistory: http.get('/ghost/api/admin/stats/member_count/', () => {
128+
return HttpResponse.json(fixtures.memberCountHistory);
129+
}),
130+
131+
browseMrrHistory: http.get('/ghost/api/admin/stats/mrr/', () => {
132+
return HttpResponse.json(fixtures.mrrHistory);
133+
})
134+
};
135+
136+
/**
137+
* Creates an MSW server with common Ghost API handlers
138+
*/
139+
export function createMswServer(additionalHandlers: Array<ReturnType<typeof http.get | typeof http.post | typeof http.put | typeof http.delete>> = []) {
140+
return setupServer(
141+
...Object.values(handlers),
142+
...additionalHandlers
143+
);
144+
}
145+
146+
/**
147+
* Sets up MSW server for testing with common lifecycle management
148+
*/
149+
export function setupMswServer(additionalHandlers: Array<ReturnType<typeof http.get | typeof http.post | typeof http.put | typeof http.delete>> = []) {
150+
const server = createMswServer(additionalHandlers);
151+
152+
beforeAll(() => server.listen());
153+
afterEach(() => {
154+
server.resetHandlers();
155+
});
156+
afterAll(() => server.close());
157+
158+
return server;
159+
}
160+
161+
/**
162+
* Helper to create custom handlers that override defaults
163+
*/
164+
export function createHandler(method: 'get' | 'post' | 'put' | 'delete', path: string, response: Record<string, unknown> | Array<unknown>, status = 200) {
165+
const httpMethod = http[method];
166+
return httpMethod(path, () => {
167+
if (status >= 400) {
168+
return new HttpResponse(null, {status});
169+
}
170+
return HttpResponse.json(response);
171+
});
172+
}
173+
174+
/**
175+
* Helper to create error handlers
176+
*/
177+
export function createErrorHandler(method: 'get' | 'post' | 'put' | 'delete', path: string, status = 500) {
178+
const httpMethod = http[method];
179+
return httpMethod(path, () => {
180+
return new HttpResponse(null, {status});
181+
});
182+
}

apps/admin-x-framework/src/test/setup.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Import and call these functions in your app's test setup file.
44
*/
55

6+
import {setupConsoleFiltering} from './test-utils';
7+
68
/**
79
* Sets up common mocks required for shade components in tests.
810
* Call this in your app's test setup file.
@@ -29,22 +31,36 @@ export function setupShadeMocks() {
2931
unobserve() {}
3032
disconnect() {}
3133
}
34+
35+
Object.defineProperty(window, 'ResizeObserver', {
36+
writable: true,
37+
value: ResizeObserverMock
38+
});
3239

33-
global.ResizeObserver = ResizeObserverMock;
34-
35-
// Mock getBoundingClientRect - provides fake dimensions for DOM elements
36-
// This prevents chart warnings and helps with layout calculations in tests
37-
const mockGetBoundingClientRect = () => ({
38-
width: 500,
39-
height: 500,
40+
// Mock getBoundingClientRect - required for positioning calculations
41+
Element.prototype.getBoundingClientRect = () => ({
42+
width: 0,
43+
height: 0,
4044
top: 0,
4145
left: 0,
42-
right: 500,
43-
bottom: 500,
46+
bottom: 0,
47+
right: 0,
4448
x: 0,
4549
y: 0,
4650
toJSON: () => {}
4751
});
52+
}
4853

49-
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect;
54+
/**
55+
* Sets up console filtering for common warnings that can't be fixed.
56+
* This is separate from setupShadeMocks so apps can choose which to use.
57+
*/
58+
export function setupConsoleFilters() {
59+
return setupConsoleFiltering({
60+
suppressReactWarnings: true,
61+
suppressChartWarnings: true,
62+
suppressMessages: [
63+
'Encountered two children with the same key'
64+
]
65+
});
5066
}

0 commit comments

Comments
 (0)