207 lines
9.7 KiB
TypeScript
207 lines
9.7 KiB
TypeScript
import { createElement } from 'react'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import InlineCode, { extractInlineCodeText, isResolvedPathType } from '../src/renderer/src/workbuddy/paths/InlineCode'
|
|
import {
|
|
getCachedResolvedType,
|
|
revalidateAndOpenPath,
|
|
subscribeToPathResolution,
|
|
type PathResolutionValue,
|
|
type PathTypeResolver
|
|
} from '../src/renderer/src/workbuddy/paths/path-resolution'
|
|
import {
|
|
detectPath,
|
|
extractPurePath,
|
|
getPathDisplayText,
|
|
hasLineRange,
|
|
isAbsolutePath,
|
|
isFilename,
|
|
isRelativePath,
|
|
isSymbol,
|
|
middleEllipsis,
|
|
parseLineRange,
|
|
shouldDetectAsPath,
|
|
truncatePathDisplay,
|
|
type PathType
|
|
} from '../src/renderer/src/workbuddy/paths/path-detection'
|
|
|
|
// Expectations are derived from REA-captured path ranges 722350-724687 and inline-code ranges 722904-723374, 727268-728626.
|
|
let checks = 0
|
|
|
|
function check(label: string, condition: boolean): void {
|
|
if (!condition) throw new Error(label)
|
|
checks++
|
|
}
|
|
|
|
function detectedType(value: string): PathType | undefined {
|
|
const detection = detectPath(value)
|
|
return detection.isPath ? detection.type : undefined
|
|
}
|
|
|
|
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void; reject: (reason?: unknown) => void } {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (reason?: unknown) => void
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
function nextTurn(): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
check('detects Windows absolute paths', isAbsolutePath('C:\\workspace\\src\\main.ts') && detectedType('C:\\workspace\\src\\main.ts') === 'file')
|
|
check('detects POSIX absolute paths', isAbsolutePath('/workspace/src/main.ts') && detectedType('/workspace/src/main.ts') === 'file')
|
|
check('detects relative paths and filename-only paths',
|
|
isRelativePath('src\\renderer\\App.tsx') && isFilename('README.md') && detectedType('README.md') === 'file')
|
|
check('classifies trailing slash paths as directories',
|
|
detectedType('/workspace/src/') === 'directory' && detectedType('C:\\workspace\\src\\') === 'directory')
|
|
|
|
const ranged = detectPath('C:\\workspace\\src\\main.ts#L12-L28')
|
|
check('parses #Lstart-Lend and extracts the pure Windows path',
|
|
ranged.isPath && ranged.type === 'file' && ranged.purePath === 'C:\\workspace\\src\\main.ts' &&
|
|
ranged.range?.start === 12 && ranged.range?.end === 28 && extractPurePath('src/main.ts#L4') === 'src/main.ts')
|
|
check('parses single-line ranges with an omitted end',
|
|
hasLineRange('/workspace/src/main.ts#L7') && parseLineRange('/workspace/src/main.ts#L7')?.start === 7 &&
|
|
parseLineRange('/workspace/src/main.ts#L7')?.end === undefined)
|
|
check('does not treat lowercase #l ranges or invalid suffixes as source ranges',
|
|
!hasLineRange('/workspace/src/main.ts#l7') && !hasLineRange('/workspace/src/main.ts#L0x'))
|
|
|
|
check('recognizes symbol identifiers and leaves unrelated strings unclassified',
|
|
isSymbol('renderMessage_2') && isSymbol('_private') && detectedType('renderMessage') === 'symbol' &&
|
|
!shouldDetectAsPath('ordinary words'))
|
|
check('symbols without a resolver result are not resolved path types',
|
|
!isResolvedPathType(undefined) && !isResolvedPathType('unknown') && isResolvedPathType('symbol'))
|
|
|
|
check('middle ellipsis uses balanced retained text and exact length',
|
|
middleEllipsis('abcdefghijklmnopqrst', 10) === 'abcd...rst')
|
|
const truncatedPath = truncatePathDisplay('C:\\very\\long\\path\\to\\file.ts', 20)
|
|
check('path truncation preserves the basename when space allows',
|
|
truncatedPath === 'C:\\very\\lo...file.ts' && truncatedPath.length === 20)
|
|
check('path display shows a file basename and keeps its line suffix',
|
|
getPathDisplayText('C:\\repo\\src\\file.ts#L12-L14', 'C:\\repo\\src\\file.ts', 'file') === 'file.ts#L12-L14')
|
|
check('path display truncates directory and symbol text',
|
|
getPathDisplayText('/workspace/a/very/long/path', '/workspace/a/very/long/path', 'directory') ===
|
|
truncatePathDisplay('/workspace/a/very/long/path') && getPathDisplayText('renderMessage', 'renderMessage', 'symbol') === 'renderMessage')
|
|
|
|
const nested = createElement('span', null, 'src/', createElement('code', null, 'App.tsx'), 3)
|
|
check('extracts code text recursively from React children', extractInlineCodeText(nested) === 'src/App.tsx3')
|
|
|
|
let nativeHandlerCalls = 0
|
|
const ssr = renderToStaticMarkup(createElement(InlineCode, {
|
|
children: 'C:\\repo\\src\\main.ts#L12-L18',
|
|
className: 'custom-code',
|
|
isFinished: true,
|
|
pathClickHandler: () => { nativeHandlerCalls++ },
|
|
resolveCode: async () => 'file' as const,
|
|
requestId: 'request-1',
|
|
paths: ['C:\\repo\\src\\main.ts']
|
|
}))
|
|
check('SSR leaves unresolved inline paths plain and performs no native action',
|
|
ssr.includes('class="cr-inline-code custom-code"') && ssr.includes('C:\\repo\\src\\main.ts#L12-L18') &&
|
|
!ssr.includes('cr-clickable-path') && nativeHandlerCalls === 0)
|
|
|
|
const streamedCode = 'C:\\repo\\src\\stream.ts'
|
|
subscribeToPathResolution('stream-request', streamedCode, [], async () => 'file' as const, () => undefined)
|
|
await nextTurn()
|
|
let streamedNativeCalls = 0
|
|
const cachedSsr = renderToStaticMarkup(createElement(InlineCode, {
|
|
children: streamedCode,
|
|
isFinished: false,
|
|
pathClickHandler: () => { streamedNativeCalls++ },
|
|
resolveCode: async () => 'unknown' as const,
|
|
requestId: 'stream-request'
|
|
}))
|
|
check('does not gate already-resolved inline paths on isFinished and SSR performs no native action',
|
|
cachedSsr.includes('cr-clickable-path cr-path-type-file') && cachedSsr.includes('title="C:\\repo\\src\\stream.ts"') &&
|
|
streamedNativeCalls === 0)
|
|
|
|
const sharedRequest = deferred<PathResolutionValue>()
|
|
const sharedCode = 'C:\\repo\\src\\shared.ts'
|
|
const sharedPaths = ['C:\\repo\\src\\shared.ts']
|
|
let resolverCalls = 0
|
|
const firstResults: PathResolutionValue[] = []
|
|
const secondResults: PathResolutionValue[] = []
|
|
const resolver: PathTypeResolver = () => {
|
|
resolverCalls++
|
|
return sharedRequest.promise
|
|
}
|
|
subscribeToPathResolution('cache-request', sharedCode, sharedPaths, resolver, (type) => firstResults.push(type))
|
|
subscribeToPathResolution('cache-request', sharedCode, [...sharedPaths], resolver, (type) => secondResults.push(type))
|
|
check('deduplicates simultaneous requests with equal request id, code, and paths', resolverCalls === 1)
|
|
sharedRequest.resolve('file')
|
|
await nextTurn()
|
|
check('notifies all active subscribers and caches the resolved path type',
|
|
firstResults[0] === 'file' && secondResults[0] === 'file' &&
|
|
getCachedResolvedType('cache-request', sharedCode, sharedPaths) === 'file')
|
|
let cacheReplay: PathResolutionValue
|
|
subscribeToPathResolution('cache-request', sharedCode, sharedPaths, () => {
|
|
resolverCalls++
|
|
return Promise.resolve('directory')
|
|
}, (type) => { cacheReplay = type })
|
|
check('reuses cached resolution without another resolver call', cacheReplay! === 'file' && resolverCalls === 1)
|
|
|
|
const revalidationEvents: string[] = []
|
|
let revalidationCalls = 0
|
|
const revalidatedType = await revalidateAndOpenPath(
|
|
'cache-request',
|
|
sharedCode,
|
|
sharedPaths,
|
|
async () => {
|
|
revalidationCalls++
|
|
return 'directory' as const
|
|
},
|
|
(type) => revalidationEvents.push(`state:${type}`),
|
|
(type) => revalidationEvents.push(`open:${type}`)
|
|
)
|
|
check('click revalidation bypasses the mount cache, updates state, then opens the refreshed type',
|
|
revalidatedType === 'directory' && revalidationCalls === 1 &&
|
|
getCachedResolvedType('cache-request', sharedCode, sharedPaths) === 'directory' &&
|
|
revalidationEvents.join(',') === 'state:directory,open:directory')
|
|
|
|
const failedRevalidationEvents: string[] = []
|
|
const failedRevalidation = await revalidateAndOpenPath(
|
|
'cache-request',
|
|
sharedCode,
|
|
sharedPaths,
|
|
async () => { throw new Error('path changed before opening') },
|
|
(type) => failedRevalidationEvents.push(`state:${type}`),
|
|
(type) => failedRevalidationEvents.push(`open:${type}`)
|
|
)
|
|
check('failed click revalidation caches unknown and does not open the stale path',
|
|
failedRevalidation === 'unknown' && getCachedResolvedType('cache-request', sharedCode, sharedPaths) === 'unknown' &&
|
|
failedRevalidationEvents.join(',') === 'state:unknown')
|
|
|
|
const cancelledRequest = deferred<PathResolutionValue>()
|
|
const cancelledResults: PathResolutionValue[] = []
|
|
const cancel = subscribeToPathResolution('cancel-request', '/repo/src/item.ts', [], () => cancelledRequest.promise, (type) => cancelledResults.push(type))
|
|
cancel()
|
|
cancelledRequest.resolve('directory')
|
|
await nextTurn()
|
|
check('cancels stale component updates without discarding the shared result',
|
|
cancelledResults.length === 0 && getCachedResolvedType('cancel-request', '/repo/src/item.ts', []) === 'directory')
|
|
|
|
let rejectedCalls = 0
|
|
const rejectedResults: PathResolutionValue[] = []
|
|
const rejectResolver: PathTypeResolver = () => {
|
|
rejectedCalls++
|
|
return Promise.reject(new Error('expected resolver failure'))
|
|
}
|
|
subscribeToPathResolution('error-request', 'symbolName', [], rejectResolver, (type) => rejectedResults.push(type))
|
|
await nextTurn()
|
|
let rejectedCacheReplay: PathResolutionValue
|
|
subscribeToPathResolution('error-request', 'symbolName', [], () => {
|
|
rejectedCalls++
|
|
return Promise.resolve('symbol')
|
|
}, (type) => { rejectedCacheReplay = type })
|
|
check('caches resolver failures as unknown and suppresses repeat requests',
|
|
rejectedResults[0] === 'unknown' && rejectedCacheReplay! === 'unknown' && rejectedCalls === 1 &&
|
|
!isResolvedPathType(rejectedCacheReplay))
|
|
|
|
console.log(`Path renderer regression checks passed (${checks}).`)
|
|
}
|
|
|
|
void main()
|