Skip to content

Commit 5e83ef8

Browse files
committed
jest > node:test
1 parent b66cb28 commit 5e83ef8

15 files changed

+432
-2100
lines changed

.eslintrc.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@ extends:
55
parserOptions:
66
ecmaVersion: 2020
77
sourceType: module
8-
env:
9-
jest: true

jest.config.js

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

package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,14 @@
1616
"scripts": {
1717
"dev": "node bin/cli.js",
1818
"gctools": "gctools",
19-
"test:only": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
20-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js && yarn lint",
19+
"test:only": "node --test --experimental-test-module-mocks test/*.test.js",
20+
"test": "node --test --experimental-test-module-mocks test/*.test.js && yarn lint",
2121
"lint": "eslint . --ext .js --cache",
2222
"update": "git pull origin main && yarn"
2323
},
2424
"devDependencies": {
2525
"eslint": "8.57.0",
26-
"eslint-plugin-ghost": "3.4.4",
27-
"jest": "30.2.0",
28-
"jest-extended": "7.0.0"
26+
"eslint-plugin-ghost": "3.4.4"
2927
},
3028
"dependencies": {
3129
"@inquirer/confirm": "3.2.0",
Lines changed: 78 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,84 @@
1-
import {jest} from '@jest/globals';
2-
import errors from '@tryghost/errors';
1+
import {describe, test, mock, before, beforeEach} from 'node:test';
2+
import assert from 'node:assert/strict';
33

44
// Shared mockApi object
55
const mockApi = {
66
members: {
7-
browse: jest.fn(),
8-
edit: jest.fn()
7+
browse: mock.fn(),
8+
edit: mock.fn()
99
}
1010
};
1111

12-
describe('add-member-comp-from-csv', () => {
13-
let fsUtils;
14-
let getTiers;
15-
let addMemberCompFromCsv;
16-
let mockFsUtils;
17-
18-
beforeAll(async () => {
19-
await jest.unstable_mockModule('@tryghost/mg-fs-utils', () => ({
20-
default: {
21-
csv: {
22-
parseCSV: jest.fn()
23-
}
24-
}
25-
}));
12+
const mockParseCSV = mock.fn();
13+
const mockGetTiers = mock.fn();
14+
15+
// Mock bluebird to use native Promise — avoids V8 structured clone
16+
// serialization issues with bluebird promise objects in node:test IPC
17+
const NativePromise = Promise;
18+
NativePromise.mapSeries = async (arr, fn) => {
19+
const results = [];
20+
for (let i = 0; i < arr.length; i++) {
21+
results.push(await fn(arr[i], i));
22+
}
23+
return results;
24+
};
25+
NativePromise.delay = ms => new NativePromise(resolve => setTimeout(resolve, ms));
26+
mock.module('bluebird', {
27+
defaultExport: NativePromise
28+
});
29+
30+
mock.module('@tryghost/mg-fs-utils', {
31+
defaultExport: {
32+
csv: {
33+
parseCSV: mockParseCSV
34+
}
35+
}
36+
});
2637

27-
await jest.unstable_mockModule('@tryghost/admin-api', () => ({
28-
default: jest.fn(() => mockApi)
29-
}));
38+
mock.module('@tryghost/admin-api', {
39+
defaultExport: function GhostAdminAPI() {
40+
return mockApi;
41+
}
42+
});
43+
44+
mock.module('../lib/admin-api-call.js', {
45+
namedExports: {getTiers: mockGetTiers}
46+
});
47+
48+
// Mock @tryghost/errors to avoid serialization issues with custom error classes
49+
// in the node:test child process IPC
50+
class MockInternalServerError extends Error {
51+
constructor({message, context}) {
52+
super(message);
53+
this.context = context;
54+
}
55+
}
56+
mock.module('@tryghost/errors', {
57+
defaultExport: {InternalServerError: MockInternalServerError}
58+
});
3059

31-
await jest.unstable_mockModule('../lib/admin-api-call.js', () => ({
32-
getTiers: jest.fn()
33-
}));
60+
describe('add-member-comp-from-csv', () => {
61+
let addMemberCompFromCsv;
3462

35-
fsUtils = (await import('@tryghost/mg-fs-utils')).default;
36-
getTiers = (await import('../lib/admin-api-call.js')).getTiers;
63+
before(async () => {
3764
addMemberCompFromCsv = (await import('../tasks/add-member-comp-from-csv.js')).default;
3865
});
3966

4067
beforeEach(() => {
41-
// Reset all mock functions
42-
mockApi.members.browse.mockReset();
43-
mockApi.members.edit.mockReset();
44-
mockFsUtils = fsUtils;
45-
});
46-
47-
afterEach(() => {
48-
jest.clearAllMocks();
68+
mockApi.members.browse.mock.resetCalls();
69+
mockApi.members.edit.mock.resetCalls();
70+
mockParseCSV.mock.resetCalls();
71+
mockGetTiers.mock.resetCalls();
4972
});
5073

5174
test('should successfully process valid CSV rows', async () => {
5275
const csvData = [
5376
{email: 'test1@example.com', expireAt: '2024-12-31', tierName: 'Premium'},
5477
{email: 'test2@example.com', expireAt: '2024-12-31', tierName: 'Basic'}
5578
];
56-
mockFsUtils.csv.parseCSV.mockResolvedValue(csvData);
79+
mockParseCSV.mock.mockImplementation(() => Promise.resolve(csvData));
5780

58-
mockApi.members.browse.mockImplementation(({filter}) => {
81+
mockApi.members.browse.mock.mockImplementation(({filter}) => {
5982
if (filter === 'email:test1@example.com') {
6083
return Promise.resolve([{id: '1', email: 'test1@example.com'}]);
6184
}
@@ -69,9 +92,9 @@ describe('add-member-comp-from-csv', () => {
6992
{id: 'tier1', name: 'Premium'},
7093
{id: 'tier2', name: 'Basic'}
7194
];
72-
getTiers.mockImplementation(() => Promise.resolve(tiers));
95+
mockGetTiers.mock.mockImplementation(() => Promise.resolve(tiers));
7396

74-
mockApi.members.edit.mockImplementation(({id}) => {
97+
mockApi.members.edit.mock.mockImplementation(({id}) => {
7598
return Promise.resolve({
7699
id,
77100
email: id === '1' ? 'test1@example.com' : 'test2@example.com',
@@ -87,69 +110,70 @@ describe('add-member-comp-from-csv', () => {
87110

88111
await task.run();
89112

90-
expect(mockApi.members.browse).toHaveBeenCalledTimes(2);
91-
expect(mockApi.members.edit).toHaveBeenCalledTimes(2);
113+
assert.strictEqual(mockApi.members.browse.mock.callCount(), 2);
114+
assert.strictEqual(mockApi.members.edit.mock.callCount(), 2);
92115
});
93116

94117
test('should handle missing members', async () => {
95118
const csvData = [
96119
{email: 'nonexistent@example.com', expireAt: '2024-12-31', tierName: 'Premium'}
97120
];
98-
mockFsUtils.csv.parseCSV.mockResolvedValue(csvData);
121+
mockParseCSV.mock.mockImplementation(() => Promise.resolve(csvData));
99122

100-
mockApi.members.browse.mockResolvedValue([]);
123+
mockApi.members.browse.mock.mockImplementation(() => Promise.resolve([]));
101124

102125
const task = addMemberCompFromCsv.getTaskRunner({
103126
apiURL: 'http://localhost:2368',
104127
adminAPIKey: 'test-key',
105128
csvPath: 'test.csv'
106129
});
107130

108-
await expect(task.run()).rejects.toThrow('Failed to process some rows');
131+
await assert.rejects(task.run(), /Failed to process some rows/);
109132
});
110133

111134
test('should handle missing tiers', async () => {
112135
const csvData = [
113136
{email: 'test@example.com', expireAt: '2024-12-31', tierName: 'NonexistentTier'}
114137
];
115-
mockFsUtils.csv.parseCSV.mockResolvedValue(csvData);
138+
mockParseCSV.mock.mockImplementation(() => Promise.resolve(csvData));
116139

117-
mockApi.members.browse.mockResolvedValue([{id: '1', email: 'test@example.com'}]);
140+
mockApi.members.browse.mock.mockImplementation(() => Promise.resolve([{id: '1', email: 'test@example.com'}]));
118141

119142
const tiers = [{id: 'tier1', name: 'Premium'}];
120-
getTiers.mockImplementation(() => Promise.resolve(tiers));
143+
mockGetTiers.mock.mockImplementation(() => Promise.resolve(tiers));
121144

122145
const task = addMemberCompFromCsv.getTaskRunner({
123146
apiURL: 'http://localhost:2368',
124147
adminAPIKey: 'test-key',
125148
csvPath: 'test.csv'
126149
});
127150

128-
await expect(task.run()).rejects.toThrow('Failed to process some rows');
151+
await assert.rejects(task.run(), /Failed to process some rows/);
129152
});
130153

131154
test('should handle API errors', async () => {
132155
const csvData = [
133156
{email: 'test@example.com', expireAt: '2024-12-31', tierName: 'Premium'}
134157
];
135-
mockFsUtils.csv.parseCSV.mockResolvedValue(csvData);
158+
mockParseCSV.mock.mockImplementation(() => Promise.resolve(csvData));
136159

137-
mockApi.members.browse.mockResolvedValue([{id: '1', email: 'test@example.com'}]);
160+
mockApi.members.browse.mock.mockImplementation(() => Promise.resolve([{id: '1', email: 'test@example.com'}]));
138161

139162
const tiers = [{id: 'tier1', name: 'Premium'}];
140-
getTiers.mockImplementation(() => Promise.resolve(tiers));
163+
mockGetTiers.mock.mockImplementation(() => Promise.resolve(tiers));
141164

142-
mockApi.members.edit.mockRejectedValue(new errors.ValidationError({
165+
const validationError = new MockInternalServerError({
143166
message: 'Invalid data',
144167
context: 'The provided data is invalid'
145-
}));
168+
});
169+
mockApi.members.edit.mock.mockImplementation(() => Promise.reject(validationError));
146170

147171
const task = addMemberCompFromCsv.getTaskRunner({
148172
apiURL: 'http://localhost:2368',
149173
adminAPIKey: 'test-key',
150174
csvPath: 'test.csv'
151175
});
152176

153-
await expect(task.run()).rejects.toThrow('Failed to process some rows');
177+
await assert.rejects(task.run(), /Failed to process some rows/);
154178
});
155-
});
179+
});

0 commit comments

Comments
 (0)