1- import { describe , expect , it , vi } from 'vitest'
1+ import { describe , expect , it , vi , beforeEach , afterEach } from 'vitest'
22import {
33 sum ,
44 chunkIntoWeeks ,
@@ -12,6 +12,10 @@ import {
1212 copyAltTextForTrendLineChart ,
1313 createAltTextForVersionsBarChart ,
1414 copyAltTextForVersionsBarChart ,
15+ loadFile ,
16+ sanitise ,
17+ insertLineBreaks ,
18+ applyEllipsis ,
1519 type TrendLineConfig ,
1620 type TrendLineDataset ,
1721 type VersionsBarConfig ,
@@ -1236,3 +1240,216 @@ describe('copyAltTextForVersionsBarChart', () => {
12361240 expect ( copyMock ) . toHaveBeenCalledWith ( expected )
12371241 } )
12381242} )
1243+
1244+ describe ( 'loadFile' , ( ) => {
1245+ let createElementMock : ReturnType < typeof vi . fn >
1246+ let clickMock : ReturnType < typeof vi . fn >
1247+ let removeMock : ReturnType < typeof vi . fn >
1248+ let originalDocument : typeof globalThis . document | undefined
1249+
1250+ beforeEach ( ( ) => {
1251+ clickMock = vi . fn ( )
1252+ removeMock = vi . fn ( )
1253+
1254+ createElementMock = vi . fn ( ) . mockReturnValue ( {
1255+ href : '' ,
1256+ download : '' ,
1257+ click : clickMock ,
1258+ remove : removeMock ,
1259+ } )
1260+
1261+ originalDocument = globalThis . document
1262+
1263+ Object . defineProperty ( globalThis , 'document' , {
1264+ value : {
1265+ createElement : createElementMock ,
1266+ } ,
1267+ configurable : true ,
1268+ writable : true ,
1269+ } )
1270+ } )
1271+
1272+ afterEach ( ( ) => {
1273+ vi . restoreAllMocks ( )
1274+
1275+ Object . defineProperty ( globalThis , 'document' , {
1276+ value : originalDocument ,
1277+ configurable : true ,
1278+ writable : true ,
1279+ } )
1280+ } )
1281+
1282+ it ( 'creates an anchor element and triggers a download' , ( ) => {
1283+ const link = 'https://npmx.dev/file.png'
1284+ const filename = 'file.png'
1285+ loadFile ( link , filename )
1286+ expect ( createElementMock ) . toHaveBeenCalledWith ( 'a' )
1287+ const anchor = createElementMock . mock . results [ 0 ] ?. value as HTMLAnchorElement
1288+ expect ( anchor . href ) . toBe ( link )
1289+ expect ( anchor . download ) . toBe ( filename )
1290+ expect ( clickMock ) . toHaveBeenCalledTimes ( 1 )
1291+ expect ( removeMock ) . toHaveBeenCalledTimes ( 1 )
1292+ } )
1293+ } )
1294+
1295+ describe ( 'sanitise' , ( ) => {
1296+ it ( 'returns the same string when no sanitisation is needed' , ( ) => {
1297+ expect ( sanitise ( 'nuxt-package' ) ) . toBe ( 'nuxt-package' )
1298+ } )
1299+
1300+ it ( 'removes a leading @ character' , ( ) => {
1301+ expect ( sanitise ( '@nuxt/ui' ) ) . toBe ( 'nuxt-ui' )
1302+ } )
1303+
1304+ it ( 'removes only the first leading @ character' , ( ) => {
1305+ expect ( sanitise ( '@@scope/package' ) ) . toBe ( '@scope-package' )
1306+ } )
1307+
1308+ it ( 'replaces forward slashes with dashes' , ( ) => {
1309+ expect ( sanitise ( 'scope/package/name' ) ) . toBe ( 'scope-package-name' )
1310+ } )
1311+
1312+ it ( 'replaces backslashes with dashes' , ( ) => {
1313+ expect ( sanitise ( 'scope\\package\\name' ) ) . toBe ( 'scope-package-name' )
1314+ } )
1315+
1316+ it ( 'replaces colon characters with dashes' , ( ) => {
1317+ expect ( sanitise ( 'name:with:colons' ) ) . toBe ( 'name-with-colons' )
1318+ } )
1319+
1320+ it ( 'replaces invalid filename characters with dashes' , ( ) => {
1321+ expect ( sanitise ( 'na<me>:"with"*?pipes|' ) ) . toBe ( 'na-me---with---pipes-' )
1322+ } )
1323+
1324+ it ( 'handles scoped package names correctly' , ( ) => {
1325+ expect ( sanitise ( '@scope/package' ) ) . toBe ( 'scope-package' )
1326+ } )
1327+
1328+ it ( 'replaces mixed invalid characters in a single string' , ( ) => {
1329+ expect ( sanitise ( '@scope/package:name*test?value<foo>|bar' ) ) . toBe (
1330+ 'scope-package-name-test-value-foo--bar' ,
1331+ )
1332+ } )
1333+
1334+ it ( 'returns an empty string when given an empty string' , ( ) => {
1335+ expect ( sanitise ( '' ) ) . toBe ( '' )
1336+ } )
1337+ } )
1338+
1339+ describe ( 'insertLineBreaks' , ( ) => {
1340+ it ( 'returns an empty string when text is not a string' , ( ) => {
1341+ expect ( insertLineBreaks ( null as unknown as string ) ) . toBe ( '' )
1342+ expect ( insertLineBreaks ( undefined as unknown as string ) ) . toBe ( '' )
1343+ expect ( insertLineBreaks ( 42 as unknown as string ) ) . toBe ( '' )
1344+ expect ( insertLineBreaks ( { } as unknown as string ) ) . toBe ( '' )
1345+ } )
1346+
1347+ it ( 'returns the original text when maxCharactersPerLine is not a positive integer' , ( ) => {
1348+ expect ( insertLineBreaks ( 'hello world' , 0 ) ) . toBe ( 'hello world' )
1349+ expect ( insertLineBreaks ( 'hello world' , - 1 ) ) . toBe ( 'hello world' )
1350+ expect ( insertLineBreaks ( 'hello world' , 2.5 ) ) . toBe ( 'hello world' )
1351+ expect ( insertLineBreaks ( 'hello world' , Number . NaN ) ) . toBe ( 'hello world' )
1352+ } )
1353+
1354+ it ( 'returns the same text when it already fits on one line' , ( ) => {
1355+ expect ( insertLineBreaks ( 'hello world' , 24 ) ) . toBe ( 'hello world' )
1356+ } )
1357+
1358+ it ( 'breaks text into multiple lines on word boundaries' , ( ) => {
1359+ expect ( insertLineBreaks ( 'hello world again' , 11 ) ) . toBe ( 'hello world\nagain' )
1360+ } )
1361+
1362+ it ( 'preserves a single space between words when collapsing whitespace' , ( ) => {
1363+ expect ( insertLineBreaks ( 'hello world' , 24 ) ) . toBe ( 'hello world' )
1364+ } )
1365+
1366+ it ( 'ignores leading and trailing whitespace' , ( ) => {
1367+ expect ( insertLineBreaks ( ' hello world ' , 24 ) ) . toBe ( 'hello world' )
1368+ } )
1369+
1370+ it ( 'handles tabs and newlines as whitespace separators' , ( ) => {
1371+ expect ( insertLineBreaks ( 'hello\tworld\nagain' , 11 ) ) . toBe ( 'hello world\nagain' )
1372+ } )
1373+
1374+ it ( 'starts a new line when adding a word would exceed the limit' , ( ) => {
1375+ expect ( insertLineBreaks ( 'one two three' , 7 ) ) . toBe ( 'one two\nthree' )
1376+ } )
1377+
1378+ it ( 'keeps a word on the current line when it exactly matches the limit' , ( ) => {
1379+ expect ( insertLineBreaks ( 'abc def' , 7 ) ) . toBe ( 'abc def' )
1380+ } )
1381+
1382+ it ( 'splits a long token into chunks when it exceeds the limit' , ( ) => {
1383+ expect ( insertLineBreaks ( 'abcdefghijkl' , 5 ) ) . toBe ( 'abcde\nfghij\nkl' )
1384+ } )
1385+
1386+ it ( 'pushes the current line before splitting a long token' , ( ) => {
1387+ expect ( insertLineBreaks ( 'hello abcdefghij' , 5 ) ) . toBe ( 'hello\nabcde\nfghij' )
1388+ } )
1389+
1390+ it ( 'continues building lines after a split long token' , ( ) => {
1391+ expect ( insertLineBreaks ( 'abcdefghij klm nop' , 5 ) ) . toBe ( 'abcde\nfghij\nklm\nnop' )
1392+ } )
1393+
1394+ it ( 'handles multiple consecutive long tokens' , ( ) => {
1395+ expect ( insertLineBreaks ( 'abcdefghijk lmnopqrs' , 4 ) ) . toBe ( 'abcd\nefgh\nijk\nlmno\npqrs' )
1396+ } )
1397+
1398+ it ( 'returns an empty string for an empty input string' , ( ) => {
1399+ expect ( insertLineBreaks ( '' , 24 ) ) . toBe ( '' )
1400+ } )
1401+
1402+ it ( 'returns an empty string for a whitespace-only string' , ( ) => {
1403+ expect ( insertLineBreaks ( ' ' , 24 ) ) . toBe ( '' )
1404+ expect ( insertLineBreaks ( '\n\t ' , 24 ) ) . toBe ( '' )
1405+ } )
1406+
1407+ it ( 'uses the default maxCharactersPerLine value when omitted' , ( ) => {
1408+ expect ( insertLineBreaks ( 'one two three four five six' ) ) . toBe ( 'one two three four five\nsix' )
1409+ } )
1410+ } )
1411+
1412+ describe ( 'applyEllipsis' , ( ) => {
1413+ it ( 'returns an empty string when text is not a string' , ( ) => {
1414+ expect ( applyEllipsis ( null as unknown as string ) ) . toBe ( '' )
1415+ expect ( applyEllipsis ( undefined as unknown as string ) ) . toBe ( '' )
1416+ expect ( applyEllipsis ( 42 as unknown as string ) ) . toBe ( '' )
1417+ expect ( applyEllipsis ( { } as unknown as string ) ) . toBe ( '' )
1418+ } )
1419+
1420+ it ( 'returns the original text when maxLength is not a positive integer' , ( ) => {
1421+ expect ( applyEllipsis ( 'touching grass' , 0 ) ) . toBe ( 'touching grass' )
1422+ expect ( applyEllipsis ( 'touching grass' , - 1 ) ) . toBe ( 'touching grass' )
1423+ expect ( applyEllipsis ( 'touching grass' , 2.5 ) ) . toBe ( 'touching grass' )
1424+ expect ( applyEllipsis ( 'touching grass' , Number . NaN ) ) . toBe ( 'touching grass' )
1425+ } )
1426+
1427+ it ( 'returns the original text when its length is less than maxLength' , ( ) => {
1428+ expect ( applyEllipsis ( 'grass' , 10 ) ) . toBe ( 'grass' )
1429+ } )
1430+
1431+ it ( 'returns the original text when its length is equal to maxLength' , ( ) => {
1432+ expect ( applyEllipsis ( 'grass' , 5 ) ) . toBe ( 'grass' )
1433+ } )
1434+
1435+ it ( 'truncates the text and appends an ellipsis when its length exceeds maxLength' , ( ) => {
1436+ expect ( applyEllipsis ( 'grass touching' , 5 ) ) . toBe ( 'grass...' )
1437+ } )
1438+
1439+ it ( 'uses the default maxLength when omitted' , ( ) => {
1440+ const text = 'n' . repeat ( 46 )
1441+ expect ( applyEllipsis ( text ) ) . toBe ( `${ 'n' . repeat ( 45 ) } ...` )
1442+ } )
1443+
1444+ it ( 'returns an empty string for an empty input string' , ( ) => {
1445+ expect ( applyEllipsis ( '' ) ) . toBe ( '' )
1446+ } )
1447+
1448+ it ( 'handles maxLength equal to 1' , ( ) => {
1449+ expect ( applyEllipsis ( 'grass' , 1 ) ) . toBe ( 'g...' )
1450+ } )
1451+
1452+ it ( 'preserves whitespace within the truncated portion' , ( ) => {
1453+ expect ( applyEllipsis ( 'you need to touch grass' , 13 ) ) . toBe ( 'you need to t...' )
1454+ } )
1455+ } )
0 commit comments