Skip to content

Commit a5c756d

Browse files
chore: more tests
1 parent c8cc3eb commit a5c756d

File tree

4 files changed

+548
-5
lines changed

4 files changed

+548
-5
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@types/lodash.camelcase": "^4.3.9",
3535
"@types/node": "^22.10.7",
3636
"@types/pretty-ms": "^5.0.1",
37+
"@vitest/coverage-v8": "3.0.5",
3738
"husky": "^9.1.6",
3839
"lint-staged": "^15.2.10",
3940
"prettier": "^3.3.3",

tests/helpers.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,44 @@ describe('extendError', () => {
1313
const newError = extendError('foo', new Error('test'));
1414
expect(newError.stack!.split('\n')[0]).toEqual(chalk.red('foo'));
1515
});
16+
17+
it('should handle errors without a stack trace', () => {
18+
const errorWithoutStack = new Error('test');
19+
delete (errorWithoutStack as any).stack;
20+
const newError = extendError('foo', errorWithoutStack);
21+
expect(newError.message).toEqual('foo - test');
22+
expect(newError.stack).toBeUndefined();
23+
});
24+
25+
it('should handle errors with non-string stack', () => {
26+
const errorWithNonStringStack: any = new Error('test');
27+
errorWithNonStringStack.stack = null;
28+
const newError = extendError('foo', errorWithNonStringStack);
29+
expect(newError.message).toEqual('foo - test');
30+
expect(newError.stack).toBeNull();
31+
});
32+
33+
it('should handle error-like objects with custom properties', () => {
34+
const customError = { message: 'custom error', stack: 'custom stack trace' };
35+
const newError = extendError('prefix', customError);
36+
expect(newError.message).toEqual('prefix - custom error');
37+
expect(newError.stack).toContain(chalk.red('prefix'));
38+
expect(newError.stack).toContain('custom stack trace');
39+
});
40+
41+
it('should handle error-like objects without a message property', () => {
42+
const errorWithoutMessage: any = { stack: 'some stack' };
43+
const newError = extendError('prefix', errorWithoutMessage);
44+
expect(newError.message).toEqual('prefix - undefined');
45+
});
46+
47+
it('should preserve original stack when it is a string', () => {
48+
const originalError = new Error('original');
49+
const originalStack = originalError.stack;
50+
const newError = extendError('wrapped', originalError);
51+
expect(newError.stack).toContain(chalk.red('wrapped'));
52+
if (originalStack) {
53+
expect(newError.stack).toContain(originalStack.split('\n').slice(1).join('\n'));
54+
}
55+
});
1656
});

tests/markdown-helpers.spec.ts

Lines changed: 290 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,10 @@ Second level methods.`;
10641064
});
10651065

10661066
it('should handle nested generics', () => {
1067-
const result = safelySeparateTypeStringOn('Map<string, Record<string | number>> | Array', '|');
1067+
const result = safelySeparateTypeStringOn(
1068+
'Map<string, Record<string | number>> | Array',
1069+
'|',
1070+
);
10681071
expect(result).toEqual(['Map<string, Record<string | number>>', 'Array']);
10691072
});
10701073

@@ -1162,5 +1165,291 @@ Second level methods.`;
11621165
expect(typedKeys.keys[0].key).toBe('options');
11631166
// The nested structure is complex - just verify we got the top-level key
11641167
});
1168+
1169+
it('should handle list items with invalid structure but nested list', () => {
1170+
const md = `
1171+
* Some description without proper format
1172+
* \`validKey\` String - Valid nested key
1173+
* \`anotherKey\` Number - Another valid key
1174+
`;
1175+
const tokens = getTokens(md);
1176+
const list = findNextList(tokens);
1177+
const typedKeys = convertListToTypedKeys(list!);
1178+
1179+
expect(typedKeys.keys).toHaveLength(2);
1180+
expect(typedKeys.keys[0].key).toBe('validKey');
1181+
expect(typedKeys.keys[1].key).toBe('anotherKey');
1182+
});
1183+
});
1184+
1185+
describe('extractStringEnum', () => {
1186+
it('should handle unexpected token at start', () => {
1187+
const result = extractStringEnum('values includes @invalid');
1188+
expect(result).toBeNull();
1189+
});
1190+
1191+
it('should throw on unexpected separator', () => {
1192+
expect(() => extractStringEnum('values includes "foo" @ "bar"')).toThrow(
1193+
/Unexpected separator token/,
1194+
);
1195+
});
1196+
1197+
it('should throw on unexpected token after quote close', () => {
1198+
expect(() => extractStringEnum('values includes "foo"!')).toThrow(
1199+
/Unexpected separator token/,
1200+
);
1201+
});
1202+
1203+
it('should throw on unclosed quote', () => {
1204+
expect(() => extractStringEnum('values includes "foo')).toThrow(
1205+
/Unexpected early termination/,
1206+
);
1207+
});
1208+
1209+
it('should return null if no values found', () => {
1210+
const result = extractStringEnum('can be ');
1211+
expect(result).toBeNull();
1212+
});
1213+
1214+
it('should handle strikethrough wrapped deprecated values', () => {
1215+
const result = extractStringEnum('values includes "foo", ~"bar"~');
1216+
expect(result).toHaveLength(2);
1217+
expect(result![0].value).toBe('foo');
1218+
expect(result![1].value).toBe('bar');
1219+
});
1220+
1221+
it('should throw on mismatched wrapper unwrapping', () => {
1222+
expect(() => extractStringEnum('values includes ~"foo"!')).toThrow(
1223+
/Expected an unwrapping token that matched/,
1224+
);
1225+
});
1226+
1227+
it('should handle terminating with period', () => {
1228+
const result = extractStringEnum('can be "foo".');
1229+
expect(result).toHaveLength(1);
1230+
expect(result![0].value).toBe('foo');
1231+
});
1232+
1233+
it('should handle terminating with semicolon', () => {
1234+
const result = extractStringEnum('can be "foo";');
1235+
expect(result).toHaveLength(1);
1236+
});
1237+
1238+
it('should handle terminating with hyphen', () => {
1239+
const result = extractStringEnum('can be "foo" -');
1240+
expect(result).toHaveLength(1);
1241+
});
1242+
1243+
it('should handle or an Object terminator', () => {
1244+
const result = extractStringEnum('can be "foo", or an Object');
1245+
expect(result).toHaveLength(1);
1246+
expect(result![0].value).toBe('foo');
1247+
});
1248+
1249+
it('should handle , or an Object terminator', () => {
1250+
const result = extractStringEnum('can be "foo", or an Object');
1251+
expect(result).toHaveLength(1);
1252+
});
1253+
1254+
it('should handle suffixes to ignore like (Deprecated)', () => {
1255+
const result = extractStringEnum('can be "foo" (Deprecated), "bar"');
1256+
expect(result).toHaveLength(2);
1257+
expect(result![0].value).toBe('foo');
1258+
expect(result![1].value).toBe('bar');
1259+
});
1260+
1261+
it('should gracefully terminate after comma when encountering unquoted text', () => {
1262+
const result = extractStringEnum('can be "foo", then other stuff');
1263+
expect(result).toHaveLength(1);
1264+
expect(result![0].value).toBe('foo');
1265+
});
1266+
});
1267+
1268+
describe('extractReturnType', () => {
1269+
it('should return null return type when no Returns pattern found', () => {
1270+
const tokens = getTokens('This is just a description without returns');
1271+
const result = extractReturnType(tokens);
1272+
expect(result.parsedReturnType).toBeNull();
1273+
expect(result.parsedDescription).toBe('This is just a description without returns');
1274+
});
1275+
1276+
it('should return null return type when Returns keyword present but no backticks', () => {
1277+
const tokens = getTokens('Returns something without backticks');
1278+
const result = extractReturnType(tokens);
1279+
expect(result.parsedReturnType).toBeNull();
1280+
expect(result.parsedDescription).toBe('Returns something without backticks');
1281+
});
1282+
1283+
it('should handle returns with continuous sentence', () => {
1284+
const tokens = getTokens('Returns `String` the value');
1285+
const result = extractReturnType(tokens);
1286+
expect(result.parsedReturnType).not.toBeNull();
1287+
expect(result.parsedReturnType!.type).toBe('String');
1288+
expect(result.parsedDescription).toBe('the value');
1289+
});
1290+
1291+
it('should throw error on incorrectly formatted type union', () => {
1292+
const tokens = getTokens('Returns `A` | `B`');
1293+
expect(() => extractReturnType(tokens)).toThrow(
1294+
/Type unions must be fully enclosed in backticks/,
1295+
);
1296+
});
1297+
1298+
it('should handle returns with failed list parsing', () => {
1299+
const tokens = getTokens('Returns `Object`\n\n* invalid list item');
1300+
const result = extractReturnType(tokens);
1301+
expect(result.parsedReturnType).not.toBeNull();
1302+
expect(result.parsedReturnType!.type).toBe('Object');
1303+
});
1304+
1305+
it('should handle description starting with pipe character', () => {
1306+
const tokens = getTokens('Returns `String` | another thing');
1307+
expect(() => extractReturnType(tokens)).toThrow(
1308+
/Type unions must be fully enclosed in backticks/,
1309+
);
1310+
});
1311+
});
1312+
1313+
describe('rawTypeToTypeInformation edge cases', () => {
1314+
it('should handle Function type without subTypedKeys', () => {
1315+
const result = rawTypeToTypeInformation('Function', '', null);
1316+
expect(result.type).toBe('Function');
1317+
expect(result.parameters).toEqual([]);
1318+
expect(result.returns).toBeNull();
1319+
});
1320+
1321+
it('should handle Function type with subTypedKeys', () => {
1322+
const md = `
1323+
* \`callback\` Function - The callback
1324+
* \`event\` Event - The event object
1325+
`;
1326+
const tokens = getTokens(md);
1327+
const list = findNextList(tokens);
1328+
const typedKeys = convertListToTypedKeys(list!);
1329+
1330+
const result = rawTypeToTypeInformation('Function', '', typedKeys);
1331+
expect(result.type).toBe('Function');
1332+
expect(result.parameters).toHaveLength(2);
1333+
expect(result.parameters![0].name).toBe('callback');
1334+
expect(result.parameters![1].name).toBe('event');
1335+
expect(result.returns).toBeNull();
1336+
});
1337+
1338+
it('should handle Object type without subTypedKeys', () => {
1339+
const result = rawTypeToTypeInformation('Object', '', null);
1340+
expect(result.type).toBe('Object');
1341+
expect(result.properties).toEqual([]);
1342+
});
1343+
1344+
it('should handle String type with subTypedKeys', () => {
1345+
const md = `
1346+
* \`option1\` - First option
1347+
* \`option2\` - Second option
1348+
`;
1349+
const tokens = getTokens(md);
1350+
const list = findNextList(tokens);
1351+
const typedKeys = convertListToTypedKeys(list!);
1352+
1353+
const result = rawTypeToTypeInformation('String', '', typedKeys);
1354+
expect(result.type).toBe('String');
1355+
expect(result.possibleValues).toHaveLength(2);
1356+
expect(result.possibleValues![0].value).toBe('option1');
1357+
});
1358+
1359+
it('should handle Event<> with inner type', () => {
1360+
const result = rawTypeToTypeInformation('Event<CustomEvent>', '', null);
1361+
expect(result.type).toBe('Event');
1362+
expect(result.eventPropertiesReference).toBeDefined();
1363+
expect(result.eventPropertiesReference!.type).toBe('CustomEvent');
1364+
});
1365+
1366+
it('should throw on Event<> with both inner type and parameter list', () => {
1367+
const md = `* \`foo\` String`;
1368+
const tokens = getTokens(md);
1369+
const list = findNextList(tokens);
1370+
const typedKeys = convertListToTypedKeys(list!);
1371+
1372+
expect(() => rawTypeToTypeInformation('Event<CustomEvent>', '', typedKeys)).toThrow(
1373+
/Event<> should not have declared inner types AND a parameter list/,
1374+
);
1375+
});
1376+
1377+
it('should throw on Event<> with multiple inner types', () => {
1378+
expect(() => rawTypeToTypeInformation('Event<Type1, Type2>', '', null)).toThrow(
1379+
/Event<> should have at most one inner type/,
1380+
);
1381+
});
1382+
1383+
it('should throw on Event<> without inner type or parameter list', () => {
1384+
expect(() => rawTypeToTypeInformation('Event<>', '', null)).toThrow(
1385+
/Event<> declaration without a parameter list/,
1386+
);
1387+
});
1388+
1389+
it('should handle Event<> with parameter list', () => {
1390+
const md = `* \`detail\` String - Event detail`;
1391+
const tokens = getTokens(md);
1392+
const list = findNextList(tokens);
1393+
const typedKeys = convertListToTypedKeys(list!);
1394+
1395+
const result = rawTypeToTypeInformation('Event<>', '', typedKeys);
1396+
expect(result.type).toBe('Event');
1397+
expect(result.eventProperties).toHaveLength(1);
1398+
expect(result.eventProperties![0].name).toBe('detail');
1399+
});
1400+
1401+
it('should handle Function<> with generic types', () => {
1402+
const result = rawTypeToTypeInformation('Function<String, Number, Boolean>', '', null);
1403+
expect(result.type).toBe('Function');
1404+
expect(result.parameters).toHaveLength(2);
1405+
expect(result.returns!.type).toBe('Boolean');
1406+
});
1407+
1408+
it('should handle Function<> without generic params falling back to subTypedKeys', () => {
1409+
const md = `* \`arg1\` String - First arg`;
1410+
const tokens = getTokens(md);
1411+
const list = findNextList(tokens);
1412+
const typedKeys = convertListToTypedKeys(list!);
1413+
1414+
const result = rawTypeToTypeInformation('Function<Boolean>', '', typedKeys);
1415+
expect(result.type).toBe('Function');
1416+
expect(result.parameters).toHaveLength(1);
1417+
expect(result.parameters![0].name).toBe('arg1');
1418+
expect(result.returns!.type).toBe('Boolean');
1419+
});
1420+
1421+
it('should throw on generic type without inner types', () => {
1422+
expect(() => rawTypeToTypeInformation('GenericType<>', '', null)).toThrow(
1423+
/should have at least one inner type/,
1424+
);
1425+
});
1426+
1427+
it('should handle generic types with Object inner type and subTypedKeys', () => {
1428+
const md = `* \`prop\` String - Property`;
1429+
const tokens = getTokens(md);
1430+
const list = findNextList(tokens);
1431+
const typedKeys = convertListToTypedKeys(list!);
1432+
1433+
const result = rawTypeToTypeInformation('Promise<Object>', '', typedKeys);
1434+
expect(result.type).toBe('Promise');
1435+
expect(result.innerTypes).toHaveLength(1);
1436+
expect(result.innerTypes![0].type).toBe('Object');
1437+
expect(result.innerTypes![0].properties).toHaveLength(1);
1438+
});
1439+
});
1440+
1441+
describe('findContentAfterList', () => {
1442+
it('should return content starting from heading_close when returnAllOnNoList=true and no list found', () => {
1443+
const md = `
1444+
### Heading
1445+
1446+
Some content without a list
1447+
1448+
#### Next Heading
1449+
`;
1450+
const tokens = getTokens(md);
1451+
const result = findContentAfterList(tokens, true);
1452+
expect(result.length).toBeGreaterThan(0);
1453+
});
11651454
});
11661455
});

0 commit comments

Comments
 (0)