Back to Repositories

Testing TypeScript Type Resolution Implementation in dcloudio/uni-app

This test suite validates the type resolution functionality in the uni-app TypeScript compiler, focusing on how different TypeScript types are processed and converted into runtime prop validations. It covers extensive type system features including generics, utility types, and external type imports.

Test Coverage Overview

The test suite provides comprehensive coverage of TypeScript type resolution scenarios including:

  • Basic type literals and reference types
  • Interface inheritance and merging
  • Generic type parameters
  • Utility types like Partial, Required, Pick, Omit
  • External type imports and module resolution
  • Platform-specific conditional type compilation

Implementation Analysis

The testing approach uses Jest to verify type resolution across multiple scenarios. It employs a custom resolve() helper that simulates the Vue compiler’s type analysis pipeline, allowing detailed inspection of how types are processed and transformed. The tests validate both the resolved prop types and their runtime representations.

Technical Details

  • Testing Framework: Jest
  • Key Dependencies: @vue/compiler-sfc, @babel/types
  • Test Utilities: Custom resolve() helper for type analysis
  • Mock File System: Virtual FS for testing external imports

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Comprehensive edge case coverage
  • Isolated test environments
  • Detailed error case validation
  • Platform-specific testing
  • Modular test organization

dcloudio/uni-app

packages/uni-app-uts/__tests__/android/sfc/compileScript/resolveType.spec.ts

            
import { normalize } from 'node:path'
import type { Identifier } from '@babel/types'
import { type SFCScriptCompileOptions, parse } from '@vue/compiler-sfc'
import { ScriptCompileContext } from '../../../../src/plugins/android/uvue/sfc/compiler/script/context'
import {
  inferRuntimeType,
  invalidateTypeCache,
  recordImports,
  resolveTypeElements,
} from '../../../../src/plugins/android/uvue/sfc/compiler/script/resolveType'
import { initPreContext } from '@dcloudio/uni-cli-shared'

describe('resolveType', () => {
  test('type literal', () => {
    const { props, calls } = resolve(`defineProps<{
      foo: number // property
      bar(): void // method
      'baz': string // string literal key
      (e: 'foo'): void // call signature
      (e: 'bar'): void
    }>()`)
    expect(props).toStrictEqual({
      foo: ['Number'],
      bar: ['Function'],
      baz: ['String'],
    })
    expect(calls?.length).toBe(2)
  })

  test('reference type', () => {
    expect(
      resolve(`
    type Aliased = { foo: number }
    defineProps<Aliased>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
    })
  })

  test('reference exported type', () => {
    expect(
      resolve(`
    export type Aliased = { foo: number }
    defineProps<Aliased>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
    })
  })

  test('reference interface', () => {
    expect(
      resolve(`
    interface Aliased { foo: number }
    defineProps<Aliased>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
    })
  })

  test('reference exported interface', () => {
    expect(
      resolve(`
    export interface Aliased { foo: number }
    defineProps<Aliased>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
    })
  })

  test('reference interface extends', () => {
    expect(
      resolve(`
    export interface A { a(): void }
    export interface B extends A { b: boolean }
    interface C { c: string }
    interface Aliased extends B, C { foo: number }
    defineProps<Aliased>()
    `).props
    ).toStrictEqual({
      a: ['Function'],
      b: ['Boolean'],
      c: ['String'],
      foo: ['Number'],
    })
  })

  test('reference class', () => {
    expect(
      resolve(`
    class Foo {}
    defineProps<{ foo: Foo }>()
    `).props
    ).toStrictEqual({
      foo: ['Object'],
    })
  })

  test('function type', () => {
    expect(
      resolve(`
    defineProps<(e: 'foo') => void>()
    `).calls?.length
    ).toBe(1)
  })

  test('reference function type', () => {
    expect(
      resolve(`
    type Fn = (e: 'foo') => void
    defineProps<Fn>()
    `).calls?.length
    ).toBe(1)
  })

  test('intersection type', () => {
    expect(
      resolve(`
    type Foo = { foo: number }
    type Bar = { bar: string }
    type Baz = { bar: string | boolean }
    defineProps<{ self: any } & Foo & Bar & Baz>()
    `).props
    ).toStrictEqual({
      self: ['Object'],
      foo: ['Number'],
      // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
      // preferred
      bar: ['String', 'Boolean'],
    })
  })

  // #7553
  test('union type', () => {
    expect(
      resolve(`
    interface CommonProps {
      size?: 'xl' | 'l' | 'm' | 's' | 'xs'
    }

    type ConditionalProps =
      | {
          color: 'normal' | 'primary' | 'secondary'
          appearance: 'normal' | 'outline' | 'text'
        }
      | {
          color: number
          appearance: 'outline'
          note: string
        }

    defineProps<CommonProps & ConditionalProps>()
    `).props
    ).toStrictEqual({
      size: ['String'],
      color: ['String', 'Number'],
      appearance: ['String'],
      note: ['String'],
    })
  })

  test('template string type', () => {
    expect(
      resolve(`
    type T = 'foo' | 'bar'
    type S = 'x' | 'y'
    defineProps<{
      [\`_\${T}_\${S}_\`]: string
    }>()
    `).props
    ).toStrictEqual({
      _foo_x_: ['String'],
      _foo_y_: ['String'],
      _bar_x_: ['String'],
      _bar_y_: ['String'],
    })
  })

  test('mapped types w/ string manipulation', () => {
    expect(
      resolve(`
    type T = 'foo' | 'bar'
    defineProps<{ [K in T]: string | number } & {
      [K in 'optional']?: boolean
    } & {
      [K in Capitalize<T>]: string
    } & {
      [K in Uppercase<Extract<T, 'foo'>>]: string
    } & {
      [K in \`x\${T}\`]: string
    }>()
    `).props
    ).toStrictEqual({
      foo: ['String', 'Number'],
      bar: ['String', 'Number'],
      Foo: ['String'],
      Bar: ['String'],
      FOO: ['String'],
      xfoo: ['String'],
      xbar: ['String'],
      optional: ['Boolean'],
    })
  })

  test('utility type: Partial', () => {
    expect(
      resolve(`
    type T = { foo: number, bar: string }
    defineProps<Partial<T>>()
    `).raw.props
    ).toMatchObject({
      foo: {
        optional: true,
      },
      bar: {
        optional: true,
      },
    })
  })

  test('utility type: Required', () => {
    expect(
      resolve(`
    type T = { foo?: number, bar?: string }
    defineProps<Required<T>>()
    `).raw.props
    ).toMatchObject({
      foo: {
        optional: false,
      },
      bar: {
        optional: false,
      },
    })
  })

  test('utility type: Pick', () => {
    expect(
      resolve(`
    type T = { foo: number, bar: string, baz: boolean }
    type K = 'foo' | 'bar'
    defineProps<Pick<T, K>>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
      bar: ['String'],
    })
  })

  test('utility type: Omit', () => {
    expect(
      resolve(`
    type T = { foo: number, bar: string, baz: boolean }
    type K = 'foo' | 'bar'
    defineProps<Omit<T, K>>()
    `).props
    ).toStrictEqual({
      baz: ['Boolean'],
    })
  })

  test('indexed access type (literal)', () => {
    expect(
      resolve(`
    type T = { bar: number }
    type S = { nested: { foo: T['bar'] }}
    defineProps<S['nested']>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
    })
  })

  test('indexed access type (advanced)', () => {
    expect(
      resolve(`
    type K = 'foo' | 'bar'
    type T = { foo: string, bar: number }
    type S = { foo: { foo: T[string] }, bar: { bar: string } }
    defineProps<S[K]>()
    `).props
    ).toStrictEqual({
      foo: ['String', 'Number'],
      bar: ['String'],
    })
  })

  test('indexed access type (number)', () => {
    expect(
      resolve(`
    type A = (string | number)[]
    type AA = Array<string>
    type T = [1, 'foo']
    type TT = [foo: 1, bar: 'foo']
    defineProps<{ foo: A[number], bar: AA[number], tuple: T[number], namedTuple: TT[number] }>()
    `).props
    ).toStrictEqual({
      foo: ['String', 'Number'],
      bar: ['String'],
      tuple: ['Number', 'String'],
      namedTuple: ['Number', 'String'],
    })
  })

  test('namespace', () => {
    expect(
      resolve(`
      type X = string
      namespace Foo {
        type X = number
        export namespace Bar {
          export type A = {
            foo: X
          }
        }
      }
      defineProps<Foo.Bar.A>()
    `).props
    ).toStrictEqual({
      foo: ['Number'],
    })
  })

  test('interface merging', () => {
    expect(
      resolve(`
      interface Foo {
        a: string
      }
      interface Foo {
        b: number
      }
      defineProps<{
        foo: Foo['a'],
        bar: Foo['b']
      }>()
    `).props
    ).toStrictEqual({
      foo: ['String'],
      bar: ['Number'],
    })
  })

  test('namespace merging', () => {
    expect(
      resolve(`
      namespace Foo {
        export type A = string
      }
      namespace Foo {
        export type B = number
      }
      defineProps<{
        foo: Foo.A,
        bar: Foo.B
      }>()
    `).props
    ).toStrictEqual({
      foo: ['String'],
      bar: ['Number'],
    })
  })

  test('namespace merging with other types', () => {
    expect(
      resolve(`
      namespace Foo {
        export type A = string
      }
      interface Foo {
        b: number
      }
      defineProps<{
        foo: Foo.A,
        bar: Foo['b']
      }>()
    `).props
    ).toStrictEqual({
      foo: ['String'],
      bar: ['Number'],
    })
  })

  test('enum merging', () => {
    expect(
      resolve(`
      enum Foo {
        A = 1
      }
      enum Foo {
        B = 'hi'
      }
      defineProps<{
        foo: Foo
      }>()
    `).props
    ).toStrictEqual({
      foo: ['Number', 'String'],
    })
  })

  test('typeof', () => {
    expect(
      resolve(`
      declare const a: string
      defineProps<{ foo: typeof a }>()
    `).props
    ).toStrictEqual({
      foo: ['String'],
    })
  })

  test('ExtractPropTypes (element-plus)', () => {
    const { props, raw } = resolve(
      `
      import { ExtractPropTypes } from 'vue'
      declare const props: {
        foo: StringConstructor,
        bar: {
          type: import('foo').EpPropFinalized<BooleanConstructor>,
          required: true
        }
      }
      type Props = ExtractPropTypes<typeof props>
      defineProps<Props>()
    `
    )
    expect(props).toStrictEqual({
      foo: ['String'],
      bar: ['Boolean'],
    })
    expect(raw.props.bar.optional).toBe(false)
  })

  test('ExtractPropTypes (antd)', () => {
    const { props } = resolve(
      `
      declare const props: () => {
        foo: StringConstructor,
        bar: { type: PropType<boolean> }
      }
      type Props = Partial<import('vue').ExtractPropTypes<ReturnType<typeof props>>>
      defineProps<Props>()
    `
    )
    expect(props).toStrictEqual({
      foo: ['String'],
      bar: ['Boolean'],
    })
  })

  describe('generics', () => {
    test('generic with type literal', () => {
      expect(
        resolve(`
        type Props<T> = T
        defineProps<Props<{ foo: string }>>()
      `).props
      ).toStrictEqual({
        foo: ['String'],
      })
    })

    test('generic used in intersection', () => {
      expect(
        resolve(`
        type Foo = { foo: string; }
        type Bar = { bar: number; }
        type Props<T,U> = T & U & { baz: boolean }
        defineProps<Props<Foo, Bar>>()
      `).props
      ).toStrictEqual({
        foo: ['String'],
        bar: ['Number'],
        baz: ['Boolean'],
      })
    })

    test('generic type /w generic type alias', () => {
      expect(
        resolve(`
        type Aliased<T> = Readonly<Partial<T>>
        type Props<T> = Aliased<T>
        type Foo = { foo: string; }
        defineProps<Props<Foo>>()
      `).props
      ).toStrictEqual({
        foo: ['String'],
      })
    })

    test('generic type /w aliased type literal', () => {
      expect(
        resolve(`
        type Aliased<T> = { foo: T }
        defineProps<Aliased<string>>()
      `).props
      ).toStrictEqual({
        foo: ['String'],
      })
    })

    test('generic type /w interface', () => {
      expect(
        resolve(`
        interface Props<T> {
          foo: T
        }
        type Foo = string
        defineProps<Props<Foo>>()
      `).props
      ).toStrictEqual({
        foo: ['String'],
      })
    })

    test('generic from external-file', () => {
      const files = {
        '/foo.ts': 'export type P<T> = { foo: T }',
      }
      const { props } = resolve(
        `
        import { P } from './foo'
        defineProps<P<string>>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
      })
    })
  })

  describe('external type imports', () => {
    test('relative ts', () => {
      const files = {
        '/foo.ts': 'export type P = { foo: number }',
        '/bar.d.ts':
          'type X = { bar: string }; export { X as Y };' +
          // verify that we can parse syntax that is only valid in d.ts
          'export const baz: boolean',
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo'
        import { Y as PP } from './bar'
        defineProps<P & PP>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['Number'],
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    if (process.platform === 'win32') {
      test('relative ts on Windows', () => {
        const files = {
          'C:\\Test\\FolderA\\foo.ts': 'export type P = { foo: number }',
          'C:\\Test\\FolderA\\bar.d.ts':
            'type X = { bar: string }; export { X as Y };' +
            // verify that we can parse syntax that is only valid in d.ts
            'export const baz: boolean',
          'C:\\Test\\FolderB\\buz.ts': 'export type Z = { buz: string }',
        }
        const { props, deps } = resolve(
          `
        import { P } from './foo'
        import { Y as PP } from './bar'
        import { Z as PPP } from '../FolderB/buz'
        defineProps<P & PP & PPP>()
      `,
          files,
          {},
          'C:\\Test\\FolderA\\Test.vue'
        )
        expect(props).toStrictEqual({
          foo: ['Number'],
          bar: ['String'],
          buz: ['String'],
        })
        expect(deps && [...deps].map(normalize)).toStrictEqual(
          Object.keys(files).map(normalize)
        )
      })
    }

    // #8244
    test('utility type in external file', () => {
      const files = {
        '/foo.ts': 'type A = { n?: number }; export type B = Required<A>',
      }
      const { props } = resolve(
        `
        import { B } from './foo'
        defineProps<B>()
      `,
        files
      )
      expect(props).toStrictEqual({
        n: ['Number'],
      })
    })

    test('relative vue', () => {
      const files = {
        '/foo.vue':
          '<script lang="ts">export type P = { foo: number }</script>',
        '/bar.vue':
          '<script setup lang="tsx">export type P = { bar: string }</script>',
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo.vue'
        import { P as PP } from './bar.vue'
        defineProps<P & PP>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['Number'],
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('relative (chained)', () => {
      const files = {
        '/foo.ts': `import type { P as PP } from './nested/bar.vue'
          export type P = { foo: number } & PP`,
        '/nested/bar.vue':
          '<script setup lang="ts">export type P = { bar: string }</script>',
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo'
        defineProps<P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['Number'],
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('relative (chained, re-export)', () => {
      const files = {
        '/foo.ts': `export { P as PP } from './bar'`,
        '/bar.ts': 'export type P = { bar: string }',
      }
      const { props, deps } = resolve(
        `
        import { PP as P } from './foo'
        defineProps<P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('relative (chained, export *)', () => {
      const files = {
        '/foo.ts': `export * from './bar'`,
        '/bar.ts': 'export type P = { bar: string }',
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo'
        defineProps<P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('relative (default export)', () => {
      const files = {
        '/foo.ts': `export default interface P { foo: string }`,
        '/bar.ts': `type X = { bar: string }; export default X`,
      }
      const { props, deps } = resolve(
        `
        import P from './foo'
        import X from './bar'
        defineProps<P & X>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('relative (default re-export)', () => {
      const files = {
        '/bar.ts': `export { default } from './foo'`,
        '/foo.ts': `export default interface P { foo: string }; export interface PP { bar: number }`,
        '/baz.ts': `export { PP as default } from './foo'`,
      }
      const { props, deps } = resolve(
        `
        import P from './bar'
        import PP from './baz'
        defineProps<P & PP>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
        bar: ['Number'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('relative (re-export /w same source type name)', () => {
      const files = {
        '/foo.ts': `export default interface P { foo: string }`,
        '/bar.ts': `export default interface PP { bar: number }`,
        '/baz.ts': `export { default as X } from './foo'; export { default as XX } from './bar'; `,
      }
      const { props, deps } = resolve(
        `import { X, XX } from './baz'
        defineProps<X & XX>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
        bar: ['Number'],
      })
      expect(deps && [...deps]).toStrictEqual(['/baz.ts', '/foo.ts', '/bar.ts'])
    })

    test('relative (dynamic import)', () => {
      const files = {
        '/foo.ts': `export type P = { foo: string, bar: import('./bar').N }`,
        '/bar.ts': 'export type N = number',
      }
      const { props, deps } = resolve(
        `
        defineProps<import('./foo').P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
        bar: ['Number'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    // #8339
    test('relative, .js import', () => {
      const files = {
        '/foo.d.ts':
          'import { PP } from "./bar.js"; export type P = { foo: PP }',
        '/bar.d.ts': 'export type PP = "foo" | "bar"',
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo'
        defineProps<P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    // test('ts module resolve', () => {
    //   const files = {
    //     '/node_modules/foo/package.json': JSON.stringify({
    //       types: 'index.d.ts',
    //     }),
    //     '/node_modules/foo/index.d.ts': 'export type P = { foo: number }',
    //     '/tsconfig.json': JSON.stringify({
    //       compilerOptions: {
    //         paths: {
    //           bar: ['./pp.ts'],
    //         },
    //       },
    //     }),
    //     '/pp.ts': 'export type PP = { bar: string }',
    //   }

    //   const { props, deps } = resolve(
    //     `
    //     import { P } from 'foo'
    //     import { PP } from 'bar'
    //     defineProps<P & PP>()
    //     `,
    //     files
    //   )

    //   expect(props).toStrictEqual({
    //     foo: ['Number'],
    //     bar: ['String'],
    //   })
    //   expect(deps && [...deps]).toStrictEqual([
    //     '/node_modules/foo/index.d.ts',
    //     '/pp.ts',
    //   ])
    // })

    // test('ts module resolve w/ project reference & extends', () => {
    //   const files = {
    //     '/tsconfig.json': JSON.stringify({
    //       references: [
    //         {
    //           path: './tsconfig.app.json',
    //         },
    //       ],
    //     }),
    //     '/tsconfig.app.json': JSON.stringify({
    //       include: ['**/*.ts', '**/*.vue'],
    //       extends: './tsconfig.web.json',
    //     }),
    //     '/tsconfig.web.json': JSON.stringify({
    //       compilerOptions: {
    //         composite: true,
    //         paths: {
    //           bar: ['./user.ts'],
    //         },
    //       },
    //     }),
    //     '/user.ts': 'export type User = { bar: string }',
    //   }

    //   const { props, deps } = resolve(
    //     `
    //     import { User } from 'bar'
    //     defineProps<User>()
    //     `,
    //     files
    //   )

    //   expect(props).toStrictEqual({
    //     bar: ['String'],
    //   })
    //   expect(deps && [...deps]).toStrictEqual(['/user.ts'])
    // })

    // test('ts module resolve w/ path aliased vue file', () => {
    //   const files = {
    //     '/tsconfig.json': JSON.stringify({
    //       compilerOptions: {
    //         include: ['**/*.ts', '**/*.vue'],
    //         paths: {
    //           '@/*': ['./src/*'],
    //         },
    //       },
    //     }),
    //     '/src/Foo.vue':
    //       '<script lang="ts">export type P = { bar: string }</script>',
    //   }

    //   const { props, deps } = resolve(
    //     `
    //     import { P } from '@/Foo.vue'
    //     defineProps<P>()
    //     `,
    //     files
    //   )

    //   expect(props).toStrictEqual({
    //     bar: ['String'],
    //   })
    //   expect(deps && [...deps]).toStrictEqual(['/src/Foo.vue'])
    // })

    test('global types', () => {
      const files = {
        // ambient
        '/app.d.ts':
          'declare namespace App { interface User { name: string } }',
        // module - should only respect the declare global block
        '/global.d.ts': `
          declare type PP = { bar: number }
          declare global {
            type PP = { bar: string }
          }
          export {}
        `,
      }

      const { props, deps } = resolve(`defineProps<App.User & PP>()`, files, {
        globalTypeFiles: Object.keys(files),
      })

      expect(props).toStrictEqual({
        name: ['String'],
        bar: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    // test('global types with ambient references', () => {
    //   const files = {
    //     // with references
    //     '/backend.d.ts': `
    //       declare namespace App.Data {
    //         export type AircraftData = {
    //           id: string
    //           manufacturer: App.Data.Listings.ManufacturerData
    //         }
    //       }
    //       declare namespace App.Data.Listings {
    //         export type ManufacturerData = {
    //           id: string
    //         }
    //       }
    //     `,
    //   }

    //   const { props } = resolve(`defineProps<App.Data.AircraftData>()`, files, {
    //     globalTypeFiles: Object.keys(files),
    //   })

    //   expect(props).toStrictEqual({
    //     id: ['String'],
    //     manufacturer: ['Object'],
    //   })
    // })

    // #9871
    test('shared generics with different args', () => {
      const files = {
        '/foo.ts': `export interface Foo<T> { value: T }`,
      }
      const { props } = resolve(
        `import type { Foo } from './foo'
        defineProps<Foo<string>>()`,
        files,
        undefined,
        `/One.vue`
      )
      expect(props).toStrictEqual({
        value: ['String'],
      })
      const { props: props2 } = resolve(
        `import type { Foo } from './foo'
        defineProps<Foo<number>>()`,
        files,
        undefined,
        `/Two.vue`,
        false /* do not invalidate cache */
      )
      expect(props2).toStrictEqual({
        value: ['Number'],
      })
    })

    test('preprocess with app-android', () => {
      initPreContext('app', {}, 'app-android', true)
      const files = {
        '/foo.ts': `
export type P = { 
  //#ifdef APP-ANDROID
  foo: number
  //#endif
  //#ifndef APP-ANDROID
  foo: string
  //#endif
}
`,
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo'
        defineProps<P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['Number'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })

    test('preprocess with app-ios', () => {
      initPreContext('app', {}, 'app-ios', true)
      const files = {
        '/foo.ts': `
export type P = { 
  //#ifdef APP-ANDROID
  foo: number
  //#endif
  //#ifndef APP-ANDROID
  foo: string
  //#endif
}
`,
      }
      const { props, deps } = resolve(
        `
        import { P } from './foo'
        defineProps<P>()
      `,
        files
      )
      expect(props).toStrictEqual({
        foo: ['String'],
      })
      expect(deps && [...deps]).toStrictEqual(Object.keys(files))
    })
  })

  describe('errors', () => {
    test('failed type reference', () => {
      expect(() => resolve(`defineProps<X>()`)).toThrow(
        `Unresolvable type reference`
      )
    })

    test('unsupported computed keys', () => {
      expect(() => resolve(`defineProps<{ [Foo]: string }>()`)).toThrow(
        `Unsupported computed key in type referenced by a macro`
      )
    })

    test('unsupported index type', () => {
      expect(() => resolve(`defineProps<X[K]>()`)).toThrow(
        `Unsupported type when resolving index type`
      )
    })

    test('failed import source resolve', () => {
      expect(() =>
        resolve(`import { X } from './foo'; defineProps<X>()`)
      ).toThrow(`Failed to resolve import source "./foo"`)
    })

    test('should not error on unresolved type when inferring runtime type', () => {
      expect(() => resolve(`defineProps<{ foo: T }>()`)).not.toThrow()
      expect(() => resolve(`defineProps<{ foo: T['bar'] }>()`)).not.toThrow()
      expect(() =>
        resolve(`
        import type P from 'unknown'
        defineProps<{ foo: P }>()
      `)
      ).not.toThrow()
    })

    test('error against failed extends', () => {
      expect(() =>
        resolve(`
        import type Base from 'unknown'
        interface Props extends Base {}
        defineProps<Props>()
      `)
      ).toThrow(`@vue-ignore`)
    })

    test('allow ignoring failed extends', () => {
      let res: any

      expect(
        () =>
          (res = resolve(`
        import type Base from 'unknown'
        interface Props extends /*@vue-ignore*/ Base {
          foo: string
        }
        defineProps<Props>()
      `))
      ).not.toThrow(`@vue-ignore`)

      expect(res.props).toStrictEqual({
        foo: ['String'],
      })
    })
  })
})

function resolve(
  code: string,
  files: Record<string, string> = {},
  options?: Partial<SFCScriptCompileOptions>,
  sourceFileName: string = '/Test.vue',
  invalidateCache = true
) {
  const { descriptor } = parse(`<script setup lang="ts">\n${code}\n</script>`, {
    filename: sourceFileName,
  })
  const ctx = new ScriptCompileContext(descriptor, {
    id: 'test',
    fs: {
      fileExists(file) {
        return !!(files[file] ?? files[normalize(file)])
      },
      readFile(file) {
        return files[file] ?? files[normalize(file)]
      },
    },
    ...options,
  })

  if (invalidateCache) {
    for (const file in files) {
      invalidateTypeCache(file)
    }
  }

  // ctx.userImports is collected when calling compileScript(), but we are
  // skipping that here, so need to manually register imports
  ctx.userImports = recordImports(ctx.scriptSetupAst!.body) as any

  let target: any
  for (const s of ctx.scriptSetupAst!.body) {
    if (
      s.type === 'ExpressionStatement' &&
      s.expression.type === 'CallExpression' &&
      (s.expression.callee as Identifier).name === 'defineProps'
    ) {
      target = s.expression.typeParameters!.params[0]
    }
  }
  const raw = resolveTypeElements(ctx, target)
  const props: Record<string, string[]> = {}
  for (const key in raw.props) {
    props[key] = inferRuntimeType(ctx, raw.props[key])
  }
  return {
    props,
    calls: raw.calls,
    deps: ctx.deps,
    raw,
  }
}