Skip to content

Commit

Permalink
feat: add type tests for Expo Router
Browse files Browse the repository at this point in the history
  • Loading branch information
marklawlor committed Jun 24, 2023
1 parent 04a8359 commit b253fc4
Show file tree
Hide file tree
Showing 8 changed files with 1,156 additions and 201 deletions.
23 changes: 16 additions & 7 deletions packages/@expo/cli/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ const path = require('path');
const roots = ['__mocks__', 'src'];

module.exports = {
testEnvironment: 'node',
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
rootDir: path.resolve(__dirname),
displayName: require('./package').name,
roots,
setupFiles: ['<rootDir>/e2e/setup.ts'],
clearMocks: true,
projects: [{
testEnvironment: 'node',
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
rootDir: path.resolve(__dirname),
displayName: require('./package').name,
roots,
setupFiles: ['<rootDir>/e2e/setup.ts'],
clearMocks: true,
}, {
displayName: require('./package').name + "-types",
runner: 'jest-runner-tsd',
testRegex: '/__typetests__/.*(test|spec)\\.[jt]sx?$',
rootDir: path.resolve(__dirname),
roots,
globalSetup: '<rootDir>/src/start/server/type-generation/__typetests__/generateFixtures.ts',
}]
};
5 changes: 2 additions & 3 deletions packages/@expo/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@
"@types/klaw-sync": "^6.0.0",
"@types/metro": "~0.66.1",
"@types/metro-core": "~0.66.0",
"@types/minipass": "^3.1.2",
"@types/npm-package-arg": "^6.1.0",
"@types/progress": "^2.0.5",
"@types/prompts": "^2.0.6",
"@types/send": "^0.17.1",
Expand All @@ -148,6 +146,7 @@
"nullthrows": "^1.1.1",
"structured-headers": "^0.4.1",
"taskr": "1.1.0",
"tree-kill": "^1.2.2"
"tree-kill": "^1.2.2",
"tsd": "^0.28.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe(getTypedRoutesUtils, () => {
const filepaths = [
['/app/file.tsx', true, { static: ['/file'] }],
['/app/(group)/page.tsx', true, { static: ['/(group)/page', '/page'] }],
['/app/folder/[slug].tsx', true, { dynamic: ['/folder/${CleanRoutePart<T>}'] }],
['/app/folder/[slug].tsx', true, { dynamic: ['/folder/${SingleRoutePart<T>}'] }],
[
'/app/(a,b,c)/(d,e)/page.tsx',
true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**
!.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { writeFile } from 'fs/promises';
import { join } from 'path';

import { getTemplateString } from '../routes';

type Fixture = {
staticRoutes: string[];
dynamicRoutes: string[];
dynamicRouteTemplates: string[];
};

const fixtures: Record<string, Fixture> = {
basic: {
staticRoutes: ['/apple', '/banana'],
dynamicRoutes: [
'/colors/${SingleRoutePart<T>}',
'/animals/${CatchAllRoutePart<T>}',
'/mix/${SingleRoutePart<T>}/${SingleRoutePart<T>}/${CatchAllRoutePart<T>}',
],
dynamicRouteTemplates: [
'/colors/[color]',
'/animals/[...animal]',
'/mix/[fruit]/[color]/[...animals]',
],
},
};

export default async function () {
await Promise.all(
Object.entries(fixtures).map(async ([key, value]) => {
const template = getTemplateString(
new Set(value.staticRoutes),
new Set(value.dynamicRoutes),
new Set(value.dynamicRouteTemplates)
)
// The Template produces a global module .d.ts declaration
// These replacements turn it into a local module
.replace(/declare module "expo-router" {|(^}\\Z)/, '')
.replaceAll(/export function/g, 'export declare function')
.replaceAll(/export const/g, 'export declare const')
// Remove the last `}`
.slice(0, -2);

return writeFile(join(__dirname, './fixtures/', key + '.ts'), template);
})
);

console.log('done');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expectType, expectError } from 'tsd-lite';

import { useGlobalSearchParams, useSegments, useRouter, useSearchParams } from './fixtures/basic';

// eslint-disable-next-line react-hooks/rules-of-hooks
const router = useRouter();

describe('router.push()', () => {
// router.push will return void when the type matches, otherwise it should error

describe('href', () => {
it('will error on non-urls', () => {
expectError(router.push('should-error'));
});

it('can accept an absolute url', () => {
expectType<void>(router.push('/apple'));
expectType<void>(router.push('/banana'));
});

it('can accept a ANY relative url', () => {
// We only type-check absolute urls
expectType<void>(router.push('./this/work/but/is/not/valid'));
});

it('works for dynamic urls', () => {
expectType<void>(router.push('/colors/blue'));
expectError(router.push('/colors/blue/test'));

expectType<void>(router.push('/animals/bear'));
expectType<void>(router.push('/animals/bear/cat/dog'));

// This will fail because it is missing params
expectError(router.push('/mix/apple'));
expectError(router.push('/mix/apple/cat'));
expectType<void>(router.push('/mix/apple/blue/cat/dog'));
});

it('can accept any external url', () => {
expectType<void>(router.push('http://expo.dev'));
});
});

describe('HrefObject', () => {
it('will error on non-urls', () => {
expectError(router.push({ pathname: 'should-error' }));
});

it('can accept an absolute url', () => {
expectType<void>(router.push({ pathname: '/apple' }));
expectType<void>(router.push({ pathname: '/banana' }));
});

it('can accept a ANY relative url', () => {
// We only type-check absolute urls
expectType<void>(router.push({ pathname: './this/work/but/is/not/valid' }));
});

it('works for dynamic urls', () => {
expectType<void>(
router.push({
pathname: '/colors/[color]',
params: { color: 'blue' },
})
);
});

it('requires a valid pathname', () => {
expectError(
router.push({
pathname: '/colors/[invalid]',
params: { color: 'blue' },
})
);
});

it('requires a valid param', () => {
expectError(
router.push({
pathname: '/colors/[color]',
params: { invalid: 'blue' },
})
);
});

it('works for catch all routes', () => {
expectType<void>(
router.push({
pathname: '/animals/[...animal]',
params: { animal: ['cat', 'dog'] },
})
);
});

it('requires an array for catch all routes', () => {
expectError(
router.push({
pathname: '/animals/[...animal]',
params: { animal: 'cat' },
})
);
});

it('works for mixed routes', () => {
expectType<void>(
router.push({
pathname: '/mix/[fruit]/[color]/[...animals]',
params: { color: 'red', fruit: 'apple', animals: ['cat', 'dog'] },
})
);
});

it('requires all params in mixed routes', () => {
expectError(
router.push({
pathname: '/mix/[fruit]/[color]/[...animals]',
params: { color: 'red', animals: ['cat', 'dog'] },
})
);
});
});
});

describe('useSearchParams', () => {
expectType<Record<'color', string>>(useSearchParams<'/colors/[color]'>());
expectType<Record<'color', string>>(useSearchParams<Record<'color', string>>());
expectError(useSearchParams<'/invalid'>());
expectError(useSearchParams<Record<'custom', string>>());
});

describe('useGlobalSearchParams', () => {
expectType<Record<'color', string>>(useGlobalSearchParams<'/colors/[color]'>());
expectType<Record<'color', string>>(useGlobalSearchParams<Record<'color', string>>());
expectError(useGlobalSearchParams<'/invalid'>());
expectError(useGlobalSearchParams<Record<'custom', string>>());
});

describe('useSegments', () => {
it('can accept an absolute url', () => {
expectType<['apple']>(useSegments<'/apple'>());
});

it('only accepts valid possible urls', () => {
expectError(useSegments<'/invalid'>());
});

it('can accept an array of segments', () => {
expectType<['apple']>(useSegments<['apple']>());
});

it('only accepts valid possible segments', () => {
expectError(useSegments<['invalid segment']>());
});
});
Loading

0 comments on commit b253fc4

Please sign in to comment.