1919 * Run: `bun run check:test-patterns`
2020 */
2121import { execFileSync } from 'node:child_process'
22- import { readFileSync , writeFileSync } from 'node:fs'
22+ import { readdirSync , readFileSync , writeFileSync } from 'node:fs'
2323import path from 'node:path'
2424import { parse } from '@babel/parser'
2525
@@ -97,7 +97,10 @@ function mockedId(node: Node): string | undefined {
9797 return first ?. type === 'StringLiteral' ? ( first as { value : string } ) . value : undefined
9898}
9999
100- /** A factory of the form `() => fooMock` (or `() => ({ ...fooMock })`) using a central mock. */
100+ /**
101+ * A factory that uses a central mock as-is: `() => fooMock`, `() => ({ ...fooMock, override })`, or
102+ * `async () => (await import('@sim/testing/mocks/foo.mock')).fooMock`.
103+ */
101104function centralMockName (
102105 factory : Node | undefined ,
103106 testingImports : Set < string >
@@ -112,9 +115,27 @@ function centralMockName(
112115 if ( spread ?. type === 'Identifier' && testingImports . has ( ( spread as { name : string } ) . name ) ) {
113116 return ( spread as { name : string } ) . name
114117 }
118+ const awaited = body . type === 'MemberExpression' ? ( body . object as Node ) : undefined
119+ if ( awaited ?. type === 'AwaitExpression' && importsTesting ( awaited . argument as Node ) ) {
120+ return ( body . property as { name : string } ) . name
121+ }
115122 return undefined
116123}
117124
125+ /** A dynamic `import('@sim/testing/…')`. */
126+ function importsTesting ( node : Node | undefined ) : boolean {
127+ const source =
128+ node ?. type === 'CallExpression' && ( node . callee as Node ) . type === 'Import'
129+ ? ( node . arguments as Node [ ] ) [ 0 ]
130+ : node ?. type === 'ImportExpression'
131+ ? ( node . source as Node )
132+ : undefined
133+ return (
134+ source ?. type === 'StringLiteral' &&
135+ ( source as { value : string } ) . value . startsWith ( '@sim/testing' )
136+ )
137+ }
138+
118139function testFiles ( ) : string [ ] {
119140 return execFileSync ( 'git' , [ 'ls-files' , '*.test.ts' , '*.test.tsx' , '*.integration.ts' ] , {
120141 cwd : ROOT ,
@@ -153,13 +174,32 @@ function canUseSharedMocks(file: string): boolean {
153174 return uses
154175}
155176
177+ /**
178+ * Module ids the central mocks declare — each `packages/testing/src/mocks/*.mock.ts` documents its
179+ * target in an `@example` `vi.mock('<id>', () => xMock)` line. Reading the declarations (not just
180+ * current usages) keeps a module covered after its last conforming usage disappears.
181+ */
182+ function declaredCentralMockIds ( ) : Set < string > {
183+ const ids = new Set < string > ( )
184+ const dir = path . join ( ROOT , 'packages/testing/src/mocks' )
185+ for ( const name of readdirSync ( dir ) ) {
186+ if ( ! name . endsWith ( '.mock.ts' ) ) continue
187+ for ( const match of readFileSync ( path . join ( dir , name ) , 'utf8' ) . matchAll (
188+ / v i \. m o c k \( \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] , \s * (?: a s y n c \s * ) ? \( \) \s * = > \s * (?: \( \{ \s * \. \. \. ) ? (?: \( a w a i t i m p o r t \( [ ^ ) ] * \) \) \. ) ? \w + M o c k \b / g
189+ ) ) {
190+ ids . add ( match [ 1 ] )
191+ }
192+ }
193+ return ids
194+ }
195+
156196function collect ( ) : Violation [ ] {
157197 const files = testFiles ( )
158198 const globals = globalMockIds ( )
159199 const parsed = files . map ( ( file ) => ( { file, program : parseFile ( file ) } ) )
160200
161201 const testingImportsByFile = new Map < string , Set < string > > ( )
162- const centralIds = new Set < string > ( globals )
202+ const centralIds = new Set < string > ( [ ... globals , ... declaredCentralMockIds ( ) ] )
163203 for ( const { file, program } of parsed ) {
164204 const imports = new Set < string > ( )
165205 for ( const statement of program . body ) {
0 commit comments