From 53da60dbb5b425e52f6734bdefc4d7e8d78f7711 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Mon, 2 Feb 2026 08:50:29 +0000 Subject: [PATCH 1/4] test: add property based tests via fast-check --- package.json | 1 + pnpm-lock.yaml | 16 ++++++ test/unit/server/utils/docs/text.spec.ts | 69 ++++++++++++++++++++++++ test/unit/shared/utils/async.spec.ts | 46 ++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/package.json b/package.json index b98455232..563192b54 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "@vitest/coverage-v8": "4.0.18", "@vue/test-utils": "2.4.6", "axe-core": "4.11.1", + "fast-check": "4.5.3", "fast-npm-meta": "1.0.0", "knip": "5.82.1", "lint-staged": "16.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52a7700f0..40f4eb745 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: axe-core: specifier: 4.11.1 version: 4.11.1 + fast-check: + specifier: 4.5.3 + version: 4.5.3 fast-npm-meta: specifier: 1.0.0 version: 1.0.0 @@ -5670,6 +5673,10 @@ packages: resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} engines: {node: '>=18'} + fast-check@4.5.3: + resolution: {integrity: sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==} + engines: {node: '>=12.17.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -7890,6 +7897,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -15789,6 +15799,10 @@ snapshots: fake-indexeddb@6.2.5: {} + fast-check@4.5.3: + dependencies: + pure-rand: 7.0.1 + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -18988,6 +19002,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@7.0.1: {} + qs@6.14.1: dependencies: side-channel: 1.1.0 diff --git a/test/unit/server/utils/docs/text.spec.ts b/test/unit/server/utils/docs/text.spec.ts index 6b77bb774..6d73f7509 100644 --- a/test/unit/server/utils/docs/text.spec.ts +++ b/test/unit/server/utils/docs/text.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import * as fc from 'fast-check' import { escapeHtml, parseJsDocLinks, @@ -40,6 +41,15 @@ describe('stripAnsi', () => { const input = `object is ReactElement${ESC}[0m${ESC}[38;5;12m<${ESC}[0mP${ESC}[38;5;12m>${ESC}[0m` expect(stripAnsi(input)).toBe('object is ReactElement

') }) + + it('should strip everything in one pass', () => { + fc.assert( + fc.property(fc.string(), input => { + const stripped = stripAnsi(input) + expect(stripAnsi(stripped)).toBe(stripped) + }), + ) + }) }) describe('escapeHtml', () => { @@ -124,6 +134,65 @@ describe('parseJsDocLinks', () => { const result = parseJsDocLinks('{@link http://example.com}', emptyLookup) expect(result).toContain('href="http://example.com"') }) + + it('should convert external URLs using {@link url} to links', () => { + fc.assert( + fc.property(fc.webUrl(), url => { + const result = parseJsDocLinks(`{@link ${url}}`, emptyLookup) + expect(result).toContain(`href="${url.replaceAll('"', '\\"')}"`) + expect(result).toContain('target="_blank"') + expect(result).toContain('rel="noreferrer"') + expect(result).toContain(escapeHtml(url)) + }), + ) + }) + + it('should convert external URLs using {@link url text} to links', () => { + fc.assert( + fc.property(fc.webUrl(), fc.stringMatching(/^[^}\s][^}]+[^}\s]$/), (url, text) => { + const result = parseJsDocLinks(`{@link ${url} ${text}}`, emptyLookup) + expect(result).toContain(`href="${url.replaceAll('"', '\\"')}"`) + expect(result).toContain('target="_blank"') + expect(result).toContain('rel="noreferrer"') + expect(result).toContain(escapeHtml(text)) + }), + ) + }) + + it('should be able to treat correctly several external URLs at the middle of a text', () => { + const surrounding = fc.stringMatching(/^[^{]*$/) + const link = fc.record({ + url: fc.webUrl(), + label: fc.option(fc.stringMatching(/^[^}\s][^}]+[^}\s]$/)), + before: surrounding, + after: surrounding, + }) + fc.assert( + fc.property(fc.array(link, { minLength: 1 }), content => { + let docString = '' + const expectedUrls = [] + for (const chunk of content) { + if (chunk.before.length !== 0 || docString.length !== 0) { + docString += `${chunk.before} ` + } + if (chunk.label === null) { + docString += `{@link ${chunk.url}}` + expectedUrls.push(chunk.url) + } else { + docString += `{@link ${chunk.url} ${chunk.label}}` + expectedUrls.push(chunk.url) + } + if (chunk.after.length !== 0) { + docString += ` ${chunk.after}` + } + } + const result = parseJsDocLinks(docString, emptyLookup) + for (const url of expectedUrls) { + expect(result).toContain(`href="${url.replaceAll('"', '\\"')}"`) + } + }), + ) + }) }) describe('renderMarkdown', () => { diff --git a/test/unit/shared/utils/async.spec.ts b/test/unit/shared/utils/async.spec.ts index 227460781..04195d652 100644 --- a/test/unit/shared/utils/async.spec.ts +++ b/test/unit/shared/utils/async.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest' +import * as fc from 'fast-check' import { mapWithConcurrency } from '../../../../shared/utils/async' describe('mapWithConcurrency', () => { @@ -92,4 +93,49 @@ describe('mapWithConcurrency', () => { // Should only have 3 concurrent since we only have 3 items expect(maxConcurrent).toBe(3) }) + + it('waits for all tasks to succeed and return them in order whatever their count and the concurrency', async () => { + await fc.assert( + fc.asyncProperty( + fc.array(fc.anything()), + fc.integer({ min: 1 }), + fc.scheduler(), + async (items, concurrency, s) => { + const fn = s.scheduleFunction(async item => item) + const results = await s.waitFor(mapWithConcurrency(items, fn, concurrency)) + expect(results).toEqual(items) + }, + ), + ) + }) + + it('not run more than concurrency tasks in parallel', async () => { + await fc.assert( + fc.asyncProperty( + fc.array(fc.anything()), // TODO, support failing tasks too + fc.integer({ min: 1 }), + fc.scheduler(), + async (items, concurrency, s) => { + let tooManyRunningTasksEncountered = false + let currentlyRunning = 0 + const fn = async (item: (typeof items)[number]) => { + currentlyRunning++ + if (currentlyRunning > concurrency) { + tooManyRunningTasksEncountered = true + } + const task = s.schedule(Promise.resolve(item)) + task.then( + // not a finally as we want to handle failing tasks too in the future + () => currentlyRunning--, + () => currentlyRunning--, + ) + return task + } + await s.waitFor(mapWithConcurrency(items, fn, concurrency)) + expect(tooManyRunningTasksEncountered).toBe(false) + }, + ), + { endOnFailure: true }, + ) + }) }) From 71a38a733aaa64b137191a26f59c1ccc8a29fbfc Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Tue, 3 Feb 2026 08:38:10 +0000 Subject: [PATCH 2/4] change expectations for jsdoc links --- test/unit/server/utils/docs/text.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/server/utils/docs/text.spec.ts b/test/unit/server/utils/docs/text.spec.ts index 6d73f7509..a928d45fe 100644 --- a/test/unit/server/utils/docs/text.spec.ts +++ b/test/unit/server/utils/docs/text.spec.ts @@ -139,7 +139,7 @@ describe('parseJsDocLinks', () => { fc.assert( fc.property(fc.webUrl(), url => { const result = parseJsDocLinks(`{@link ${url}}`, emptyLookup) - expect(result).toContain(`href="${url.replaceAll('"', '\\"')}"`) + expect(result).toContain(`href="${escapeHtml(url)}"`) expect(result).toContain('target="_blank"') expect(result).toContain('rel="noreferrer"') expect(result).toContain(escapeHtml(url)) @@ -151,7 +151,7 @@ describe('parseJsDocLinks', () => { fc.assert( fc.property(fc.webUrl(), fc.stringMatching(/^[^}\s][^}]+[^}\s]$/), (url, text) => { const result = parseJsDocLinks(`{@link ${url} ${text}}`, emptyLookup) - expect(result).toContain(`href="${url.replaceAll('"', '\\"')}"`) + expect(result).toContain(`href="${escapeHtml(url)}"`) expect(result).toContain('target="_blank"') expect(result).toContain('rel="noreferrer"') expect(result).toContain(escapeHtml(text)) @@ -188,7 +188,7 @@ describe('parseJsDocLinks', () => { } const result = parseJsDocLinks(docString, emptyLookup) for (const url of expectedUrls) { - expect(result).toContain(`href="${url.replaceAll('"', '\\"')}"`) + expect(result).toContain(`href="${escapeHtml(url)}"`) } }), ) From 29673671723c5a855062e822ae8422236fc1b012 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Tue, 3 Feb 2026 08:39:39 +0000 Subject: [PATCH 3/4] drop comment related to failure case in async --- test/unit/shared/utils/async.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/unit/shared/utils/async.spec.ts b/test/unit/shared/utils/async.spec.ts index 04195d652..4e8c2ee4f 100644 --- a/test/unit/shared/utils/async.spec.ts +++ b/test/unit/shared/utils/async.spec.ts @@ -112,7 +112,7 @@ describe('mapWithConcurrency', () => { it('not run more than concurrency tasks in parallel', async () => { await fc.assert( fc.asyncProperty( - fc.array(fc.anything()), // TODO, support failing tasks too + fc.array(fc.anything()), fc.integer({ min: 1 }), fc.scheduler(), async (items, concurrency, s) => { @@ -124,11 +124,7 @@ describe('mapWithConcurrency', () => { tooManyRunningTasksEncountered = true } const task = s.schedule(Promise.resolve(item)) - task.then( - // not a finally as we want to handle failing tasks too in the future - () => currentlyRunning--, - () => currentlyRunning--, - ) + task.then(() => currentlyRunning--) // this task always succeeds by construct return task } await s.waitFor(mapWithConcurrency(items, fn, concurrency)) From f0d047ef8743c2c8b7312092798b446db68112ac Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Tue, 3 Feb 2026 08:44:13 +0000 Subject: [PATCH 4/4] drop unwanted endOnFailure --- test/unit/shared/utils/async.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/shared/utils/async.spec.ts b/test/unit/shared/utils/async.spec.ts index 4e8c2ee4f..8c2104e4c 100644 --- a/test/unit/shared/utils/async.spec.ts +++ b/test/unit/shared/utils/async.spec.ts @@ -131,7 +131,6 @@ describe('mapWithConcurrency', () => { expect(tooManyRunningTasksEncountered).toBe(false) }, ), - { endOnFailure: true }, ) }) })