@dannysir/js-te 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +267 -0
- package/babelTransformImport.js +80 -0
- package/bin/cli.js +123 -0
- package/constants.js +28 -0
- package/index.js +89 -0
- package/package.json +49 -0
- package/src/mock/store.js +17 -0
- package/src/tests.js +30 -0
- package/utils/consoleColor.js +8 -0
- package/utils/makeMessage.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dannysir
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# js-te
|
|
2
|
+
|
|
3
|
+
Jest에서 영감을 받아 만든 가벼운 JavaScript 테스트 프레임워크입니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev js-te
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 빠른 시작
|
|
12
|
+
|
|
13
|
+
### 1. 테스트 파일 만들기
|
|
14
|
+
|
|
15
|
+
`*.test.js` 파일을 만들면 자동으로 찾아서 실행합니다.
|
|
16
|
+
|
|
17
|
+
별도의 import문 없이 `describe`와 `test`, `expect` 로직이 사용 가능합니다.
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// math.test.js
|
|
21
|
+
describe('[단순 연산 테스트]', () => {
|
|
22
|
+
test('더하기 테스트', () => {
|
|
23
|
+
expect(1 + 2).toBe(3);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. 테스트 실행
|
|
29
|
+
|
|
30
|
+
package.json에 추가:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "js-te"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
실행:
|
|
41
|
+
```bash
|
|
42
|
+
npm test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# API
|
|
47
|
+
|
|
48
|
+
## 테스트 작성
|
|
49
|
+
|
|
50
|
+
### `test(설명, 함수)`
|
|
51
|
+
|
|
52
|
+
테스트 하나를 정의합니다.
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
test('배열 길이 확인', () => {
|
|
56
|
+
expect([1, 2, 3].length).toBe(3);
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### `describe(이름, 함수)`
|
|
61
|
+
|
|
62
|
+
테스트를 그룹으로 묶습니다. 중첩도 됩니다.
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
describe('계산기', () => {
|
|
66
|
+
describe('더하기', () => {
|
|
67
|
+
test('양수 더하기', () => {
|
|
68
|
+
expect(2 + 3).toBe(5);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('빼기', () => {
|
|
73
|
+
test('양수 빼기', () => {
|
|
74
|
+
expect(5 - 3).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Matcher
|
|
81
|
+
|
|
82
|
+
### `expect(값).toBe(기댓값)`
|
|
83
|
+
|
|
84
|
+
`===`로 비교합니다. 숫자, 문자열 같은 원시값 비교할 때 사용.
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
expect(5).toBe(5);
|
|
88
|
+
expect('안녕').toBe('안녕');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `expect(값).toEqual(기댓값)`
|
|
92
|
+
|
|
93
|
+
객체나 배열의 내용을 비교합니다.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
expect({ name: '철수' }).toEqual({ name: '철수' });
|
|
97
|
+
expect([1, 2, 3]).toEqual([1, 2, 3]);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `expect(함수).toThrow(에러메시지)`
|
|
101
|
+
|
|
102
|
+
함수가 에러를 던지는지 확인합니다.
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
expect(() => {
|
|
106
|
+
throw new Error('에러 발생');
|
|
107
|
+
}).toThrow('에러');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `expect(값).toBeTruthy()` / `expect(값).toBeFalsy()`
|
|
111
|
+
|
|
112
|
+
참/거짓 여부를 확인합니다.
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
expect(true).toBeTruthy();
|
|
116
|
+
expect(0).toBeFalsy();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Mocking
|
|
120
|
+
|
|
121
|
+
### 동작 원리
|
|
122
|
+
|
|
123
|
+
Babel을 사용해서 import 구문을 변환하여 mock 함수를 가져오도록 했습니다.
|
|
124
|
+
|
|
125
|
+
1. 테스트 파일 찾기
|
|
126
|
+
1. `mock(path, mockObj)` 선언 확인
|
|
127
|
+
2. `path`를 key로 이용해 Map에 저장
|
|
128
|
+
2. Babel로 코드 변환
|
|
129
|
+
1. 전체 파일의 import문 확인
|
|
130
|
+
2. import 경로가 Map에 존재하면 mock 객체로 변환
|
|
131
|
+
3. import 경로가 Map에 없다면 그대로 import
|
|
132
|
+
3. 테스트 실행
|
|
133
|
+
4. 원본 파일 복구
|
|
134
|
+
|
|
135
|
+
### 🚨 주의 사항 (현재 수정 중)
|
|
136
|
+
|
|
137
|
+
mocking 기능의 경우 현재 `path`를 기반으로 직접 변환을 하기 때문에 mocking이 필요한 함수의 경우
|
|
138
|
+
|
|
139
|
+
반드시 **절대 경로**로 표현한 후 `mock` 함수에 **절대 경로**로 등록을 해주세요.
|
|
140
|
+
|
|
141
|
+
> 만약 모듈이 사용되는 모든 위치의 path가 동일하다면 상대 경로도 정삭 작동합니다.
|
|
142
|
+
|
|
143
|
+
### `mock(모듈경로, mock객체)`
|
|
144
|
+
|
|
145
|
+
모듈을 모킹합니다. import 하기 **전에** 호출해야 합니다.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// random.js
|
|
149
|
+
export const random = () => Math.random();
|
|
150
|
+
|
|
151
|
+
// game.js
|
|
152
|
+
import { random } from './random.js';
|
|
153
|
+
export const play = () => random() * 10;
|
|
154
|
+
|
|
155
|
+
// game.test.js
|
|
156
|
+
import { mock, test, expect } from 'js-te';
|
|
157
|
+
|
|
158
|
+
test('랜덤 함수 모킹', async () => {
|
|
159
|
+
// 1. 먼저 모킹
|
|
160
|
+
mock('./random.js', {
|
|
161
|
+
random: () => 0.5
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 2. 그 다음 import
|
|
165
|
+
const { play } = await import('./game.js');
|
|
166
|
+
|
|
167
|
+
// 3. 모킹된 값 사용
|
|
168
|
+
expect(play()).toBe(5);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `clearAllMocks()`
|
|
173
|
+
|
|
174
|
+
등록된 모든 mock을 제거합니다.
|
|
175
|
+
|
|
176
|
+
### `unmock(모듈경로)`
|
|
177
|
+
|
|
178
|
+
특정 mock만 제거합니다.
|
|
179
|
+
|
|
180
|
+
### `isMocked(모듈경로)`
|
|
181
|
+
|
|
182
|
+
mock이 등록되어 있는지 확인합니다.
|
|
183
|
+
|
|
184
|
+
## 테스트 파일 찾기 규칙
|
|
185
|
+
|
|
186
|
+
자동으로 다음 파일들을 찾아서 실행합니다:
|
|
187
|
+
|
|
188
|
+
1. `*.test.js` 파일
|
|
189
|
+
2. `test/` 폴더 안의 모든 `.js` 파일
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
프로젝트/
|
|
193
|
+
├── src/
|
|
194
|
+
│ ├── utils.js
|
|
195
|
+
│ └── utils.test.js ✅
|
|
196
|
+
├── test/
|
|
197
|
+
│ ├── integration.js ✅
|
|
198
|
+
│ └── e2e.js ✅
|
|
199
|
+
└── calculator.test.js ✅
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## 예제
|
|
203
|
+
|
|
204
|
+
### 기본 테스트
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
import { describe, test, expect } from 'js-te';
|
|
208
|
+
|
|
209
|
+
describe('문자열 테스트', () => {
|
|
210
|
+
test('문자열 합치기', () => {
|
|
211
|
+
const result = 'hello' + ' ' + 'world';
|
|
212
|
+
expect(result).toBe('hello world');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('대문자 변환', () => {
|
|
216
|
+
expect('hello'.toUpperCase()).toBe('HELLO');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 모킹 예제
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
// mocking.test.js
|
|
225
|
+
import { mock, test, expect } from 'js-te';
|
|
226
|
+
|
|
227
|
+
test('[mocking] - mocking random function', async () => {
|
|
228
|
+
mock('/src/test-helper/random.js', {
|
|
229
|
+
random: () => 3,
|
|
230
|
+
});
|
|
231
|
+
const {play} = await import('../src/test-helper/game.js');
|
|
232
|
+
expect(play()).toBe(30);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
// game.js
|
|
237
|
+
import {random} from '/src/test-helper/random.js'
|
|
238
|
+
|
|
239
|
+
export const play = () => {
|
|
240
|
+
return random() * 10;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// random.js
|
|
244
|
+
export const random = () => Math.random();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## 설정
|
|
248
|
+
|
|
249
|
+
`package.json`에 해당 설정을 하셔야 정상 작동합니다.
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"type": "module"
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 링크
|
|
258
|
+
|
|
259
|
+
- [GitHub](https://github.com/dannysir/Js-Te)
|
|
260
|
+
|
|
261
|
+
## 만든 이유
|
|
262
|
+
|
|
263
|
+
우아한테크코스 과제하면서 Jest 써보고 테스트 프레임워크가 어떻게 동작하는지 궁금해서 만들어봤습니다.
|
|
264
|
+
|
|
265
|
+
## 라이선스
|
|
266
|
+
|
|
267
|
+
ISC
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const babelTransformImport = ({types: t}) => {
|
|
2
|
+
return {
|
|
3
|
+
visitor: {
|
|
4
|
+
Program(path) {
|
|
5
|
+
const importStatement = t.ImportDeclaration(
|
|
6
|
+
[t.importSpecifier(
|
|
7
|
+
t.identifier('__mockRegistry__'),
|
|
8
|
+
t.identifier('__mockRegistry__')
|
|
9
|
+
)],
|
|
10
|
+
t.stringLiteral('js-te/src/mock/store.js')
|
|
11
|
+
);
|
|
12
|
+
path.node.body.unshift(importStatement);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
ImportDeclaration(path) {
|
|
16
|
+
const source = path.node.source.value;
|
|
17
|
+
|
|
18
|
+
if (source === 'js-te/src/mock/store.js') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const specifiers = path.node.specifiers;
|
|
23
|
+
|
|
24
|
+
// 1. 먼저 모듈을 한 번만 가져오기 (mock이면 mock, 아니면 실제)
|
|
25
|
+
const moduleVarName = path.scope.generateUidIdentifier('module');
|
|
26
|
+
|
|
27
|
+
const moduleDeclaration = t.variableDeclaration('const', [
|
|
28
|
+
t.variableDeclarator(
|
|
29
|
+
moduleVarName,
|
|
30
|
+
t.conditionalExpression(
|
|
31
|
+
// 조건: __mockRegistry__.has(source)
|
|
32
|
+
t.callExpression(
|
|
33
|
+
t.memberExpression(t.identifier('__mockRegistry__'), t.identifier('has')),
|
|
34
|
+
[t.stringLiteral(source)]
|
|
35
|
+
),
|
|
36
|
+
// true: mock 반환
|
|
37
|
+
t.callExpression(
|
|
38
|
+
t.memberExpression(t.identifier('__mockRegistry__'), t.identifier('get')),
|
|
39
|
+
[t.stringLiteral(source)]
|
|
40
|
+
),
|
|
41
|
+
// false: 실제 import
|
|
42
|
+
t.awaitExpression(
|
|
43
|
+
t.importExpression(t.stringLiteral(source))
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// 2. 각 specifier를 moduleVarName에서 추출
|
|
50
|
+
const extractDeclarations = specifiers.map(spec => {
|
|
51
|
+
let importedName, localName;
|
|
52
|
+
|
|
53
|
+
if (t.isImportDefaultSpecifier(spec)) {
|
|
54
|
+
importedName = 'default';
|
|
55
|
+
localName = spec.local.name;
|
|
56
|
+
} else if (t.isImportNamespaceSpecifier(spec)) {
|
|
57
|
+
localName = spec.local.name;
|
|
58
|
+
// namespace import는 전체 모듈
|
|
59
|
+
return t.variableDeclarator(
|
|
60
|
+
t.identifier(localName),
|
|
61
|
+
moduleVarName
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
importedName = spec.imported.name;
|
|
65
|
+
localName = spec.local.name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return t.variableDeclarator(
|
|
69
|
+
t.identifier(localName),
|
|
70
|
+
t.memberExpression(moduleVarName, t.identifier(importedName))
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const extractDeclaration = t.variableDeclaration('const', extractDeclarations);
|
|
75
|
+
|
|
76
|
+
path.replaceWithMultiple([moduleDeclaration, extractDeclaration]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {transformSync} from '@babel/core';
|
|
6
|
+
import * as jsTe from '../index.js';
|
|
7
|
+
import {green, yellow} from "../utils/consoleColor.js";
|
|
8
|
+
import {getTestResultMsg} from "../utils/makeMessage.js";
|
|
9
|
+
import {RESULT_TITLE} from "../constants.js";
|
|
10
|
+
import { babelTransformImport } from '../babelTransformImport.js';
|
|
11
|
+
|
|
12
|
+
let totalPassed = 0;
|
|
13
|
+
let totalFailed = 0;
|
|
14
|
+
const originalFiles = new Map();
|
|
15
|
+
|
|
16
|
+
Object.keys(jsTe).forEach(key => {
|
|
17
|
+
global[key] = jsTe[key];
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const transformFile = (filePath) => {
|
|
21
|
+
const originalCode = fs.readFileSync(filePath, 'utf-8');
|
|
22
|
+
originalFiles.set(filePath, originalCode);
|
|
23
|
+
|
|
24
|
+
const transformed = transformSync(originalCode, {
|
|
25
|
+
filename: filePath,
|
|
26
|
+
plugins: [babelTransformImport],
|
|
27
|
+
parserOpts: {
|
|
28
|
+
sourceType: 'module',
|
|
29
|
+
plugins: ['dynamicImport']
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(filePath, transformed.code);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const restoreFiles = () => {
|
|
37
|
+
for (const [filePath, originalCode] of originalFiles.entries()) {
|
|
38
|
+
fs.writeFileSync(filePath, originalCode);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const findTestFiles = (dir) => {
|
|
43
|
+
const files = [];
|
|
44
|
+
|
|
45
|
+
const walk = (directory, inTestDir = false) => {
|
|
46
|
+
const items = fs.readdirSync(directory);
|
|
47
|
+
const dirName = path.basename(directory);
|
|
48
|
+
|
|
49
|
+
const isTestDir = dirName === 'test' || inTestDir;
|
|
50
|
+
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (item === 'node_modules') continue;
|
|
53
|
+
|
|
54
|
+
const fullPath = path.join(directory, item);
|
|
55
|
+
const stat = fs.statSync(fullPath);
|
|
56
|
+
|
|
57
|
+
if (stat.isDirectory()) {
|
|
58
|
+
walk(fullPath, isTestDir);
|
|
59
|
+
} else if (item.endsWith('.test.js') || isTestDir) {
|
|
60
|
+
if (item.endsWith('.js')) {
|
|
61
|
+
files.push(fullPath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
walk(dir);
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const findAllSourceFiles = (dir) => {
|
|
72
|
+
const files = [];
|
|
73
|
+
|
|
74
|
+
const walk = (directory) => {
|
|
75
|
+
const items = fs.readdirSync(directory);
|
|
76
|
+
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
if (item === 'node_modules' || item === 'bin' || item === 'test') continue;
|
|
79
|
+
|
|
80
|
+
const fullPath = path.join(directory, item);
|
|
81
|
+
const stat = fs.statSync(fullPath);
|
|
82
|
+
|
|
83
|
+
if (stat.isDirectory()) {
|
|
84
|
+
walk(fullPath);
|
|
85
|
+
} else if (item.endsWith('.js') && !item.endsWith('.test.js')) {
|
|
86
|
+
files.push(fullPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
walk(dir);
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sourceFiles = findAllSourceFiles(process.cwd());
|
|
96
|
+
|
|
97
|
+
for (const file of sourceFiles) {
|
|
98
|
+
transformFile(file);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const testFiles = findTestFiles(process.cwd());
|
|
102
|
+
|
|
103
|
+
console.log(`\nFound ${green(testFiles.length)} test file(s)`);
|
|
104
|
+
|
|
105
|
+
for (const file of testFiles) {
|
|
106
|
+
console.log(`\n${yellow(file)}\n`);
|
|
107
|
+
|
|
108
|
+
transformFile(file);
|
|
109
|
+
|
|
110
|
+
await import(path.resolve(file));
|
|
111
|
+
|
|
112
|
+
const {passed, failed} = await jsTe.run();
|
|
113
|
+
totalPassed += passed;
|
|
114
|
+
totalFailed += failed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
restoreFiles();
|
|
118
|
+
|
|
119
|
+
console.log(getTestResultMsg(RESULT_TITLE.TOTAL, totalPassed, totalFailed));
|
|
120
|
+
|
|
121
|
+
if (totalFailed > 0) {
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
package/constants.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const getErrorMsg = (expect, actual) => {
|
|
2
|
+
return `Expected ${JSON.stringify(expect)} but got ${JSON.stringify(actual)}`;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const RESULT_TITLE = {
|
|
6
|
+
TESTS: 'Tests: ',
|
|
7
|
+
TOTAL : 'Total Result: '
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const CHECK = '✓ ';
|
|
11
|
+
|
|
12
|
+
export const CROSS = '✗ ';
|
|
13
|
+
|
|
14
|
+
export const DIRECTORY_DELIMITER = ' > ';
|
|
15
|
+
|
|
16
|
+
export const EMPTY = '';
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_COUNT = 0;
|
|
19
|
+
|
|
20
|
+
export const COLORS = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
gray: '\x1b[90m',
|
|
27
|
+
bold: '\x1b[1m'
|
|
28
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHECK,
|
|
3
|
+
CROSS, DEFAULT_COUNT, DIRECTORY_DELIMITER, EMPTY,
|
|
4
|
+
getErrorMsg, RESULT_TITLE,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
import {Tests} from "./src/tests.js";
|
|
7
|
+
import {green, red} from "./utils/consoleColor.js";
|
|
8
|
+
import {getTestResultMsg} from "./utils/makeMessage.js";
|
|
9
|
+
|
|
10
|
+
const tests = new Tests();
|
|
11
|
+
|
|
12
|
+
export { mock, clearAllMocks, unmock, isMocked } from './src/mock/store.js';
|
|
13
|
+
|
|
14
|
+
export const test = (description, fn) => tests.test(description, fn);
|
|
15
|
+
|
|
16
|
+
export const describe = (suiteName, fn) => tests.describe(suiteName, fn);
|
|
17
|
+
|
|
18
|
+
export const expect = (actual) => {
|
|
19
|
+
let value = actual;
|
|
20
|
+
|
|
21
|
+
const runArgFnc = (actual) => {
|
|
22
|
+
let value = actual;
|
|
23
|
+
if (typeof actual === 'function') {
|
|
24
|
+
value = actual();
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
toBe(expected) {
|
|
31
|
+
value = runArgFnc(actual);
|
|
32
|
+
if (value !== expected) {
|
|
33
|
+
throw new Error(getErrorMsg(expected, value));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
toEqual(expected) {
|
|
37
|
+
value = runArgFnc(actual);
|
|
38
|
+
if (JSON.stringify(value) !== JSON.stringify(expected)) {
|
|
39
|
+
throw new Error(getErrorMsg(expected, value));
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
toThrow(expected) {
|
|
43
|
+
try {
|
|
44
|
+
value = runArgFnc(actual);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (!e.message.includes(expected)) {
|
|
47
|
+
throw new Error(getErrorMsg(expected, e.message));
|
|
48
|
+
} else return;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
toBeTruthy() {
|
|
52
|
+
value = runArgFnc(actual);
|
|
53
|
+
if (!value) {
|
|
54
|
+
throw new Error(getErrorMsg(true, value));
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
toBeFalsy() {
|
|
58
|
+
value = runArgFnc(actual);
|
|
59
|
+
if (value) {
|
|
60
|
+
throw new Error(getErrorMsg(true, value));
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const run = async () => {
|
|
67
|
+
let passed = DEFAULT_COUNT;
|
|
68
|
+
let failed = DEFAULT_COUNT;
|
|
69
|
+
|
|
70
|
+
for (const test of tests.getTests()) {
|
|
71
|
+
try {
|
|
72
|
+
await test.fn();
|
|
73
|
+
const directoryString = green(CHECK) + (test.path === '' ? EMPTY : test.path + DIRECTORY_DELIMITER) + test.description
|
|
74
|
+
console.log(directoryString);
|
|
75
|
+
passed++;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const errorDirectory = red(CROSS) + test.path + test.description
|
|
78
|
+
console.log(errorDirectory);
|
|
79
|
+
console.log(red(` ${error.message}`));
|
|
80
|
+
failed++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(getTestResultMsg(RESULT_TITLE.TESTS, passed, failed));
|
|
85
|
+
|
|
86
|
+
tests.clearTests();
|
|
87
|
+
|
|
88
|
+
return {passed, failed};
|
|
89
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dannysir/js-te",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "JavaScript test library",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./src/mock/store.js": "./src/mock/store.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"js-te": "./bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node bin/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/dannysir/Js-Te.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"javascript",
|
|
23
|
+
"test",
|
|
24
|
+
"testing",
|
|
25
|
+
"jest",
|
|
26
|
+
"mocking"
|
|
27
|
+
],
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"src/tests.js",
|
|
31
|
+
"src/mock",
|
|
32
|
+
"utils/",
|
|
33
|
+
"index.js",
|
|
34
|
+
"constants.js",
|
|
35
|
+
"babelTransformImport.js"
|
|
36
|
+
],
|
|
37
|
+
"author": "dannysir",
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/dannysir/Js-Te/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/dannysir/Js-Te#readme",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@babel/core": "^7.28.5",
|
|
45
|
+
"@babel/generator": "^7.28.5",
|
|
46
|
+
"@babel/parser": "^7.28.5",
|
|
47
|
+
"@babel/traverse": "^7.28.5"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const __mockRegistry__ = new Map();
|
|
2
|
+
|
|
3
|
+
export function clearAllMocks() {
|
|
4
|
+
__mockRegistry__.clear();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function mock(modulePath, mockExports) {
|
|
8
|
+
__mockRegistry__.set(modulePath, mockExports);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function unmock(modulePath) {
|
|
12
|
+
__mockRegistry__.delete(modulePath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isMocked(modulePath) {
|
|
16
|
+
return __mockRegistry__.has(modulePath);
|
|
17
|
+
}
|
package/src/tests.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {DIRECTORY_DELIMITER} from "../constants.js";
|
|
2
|
+
|
|
3
|
+
export class Tests {
|
|
4
|
+
#tests = [];
|
|
5
|
+
#testDepth = [];
|
|
6
|
+
|
|
7
|
+
describe(str, fn) {
|
|
8
|
+
this.#testDepth.push(str);
|
|
9
|
+
fn();
|
|
10
|
+
this.#testDepth.pop();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test(description, fn) {
|
|
14
|
+
const testObj = {
|
|
15
|
+
description,
|
|
16
|
+
fn,
|
|
17
|
+
path: this.#testDepth.join(DIRECTORY_DELIMITER),
|
|
18
|
+
}
|
|
19
|
+
this.#tests.push(testObj);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getTests() {
|
|
23
|
+
return [...this.#tests];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clearTests() {
|
|
27
|
+
this.#tests = [];
|
|
28
|
+
this.#testDepth = [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {COLORS} from "../constants.js";
|
|
2
|
+
|
|
3
|
+
export const colorize = (text, color) => `${color}${text}${COLORS.reset}`;
|
|
4
|
+
|
|
5
|
+
export const green = (text) => colorize(text, COLORS.green + COLORS.bold);
|
|
6
|
+
export const red = (text) => colorize(text, COLORS.red + COLORS.bold);
|
|
7
|
+
export const bold = (text) => colorize(text, COLORS.bold);
|
|
8
|
+
export const yellow = (text) => colorize(text, COLORS.yellow + COLORS.bold);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {bold, green, red} from "./consoleColor.js";
|
|
2
|
+
|
|
3
|
+
export const getTestResultMsg = (title, success, fail) => {
|
|
4
|
+
let msg = '\n';
|
|
5
|
+
|
|
6
|
+
msg += title;
|
|
7
|
+
msg += green(success + ' passed') + ', ';
|
|
8
|
+
if (fail) {
|
|
9
|
+
msg += red(fail + ' failed') + ', ';
|
|
10
|
+
}
|
|
11
|
+
msg += bold(success + fail + ' total');
|
|
12
|
+
|
|
13
|
+
return msg;
|
|
14
|
+
};
|