Skip to content

Commit ae848c8

Browse files
committed
test: add-types-tests
adds types test files for many of the types files we have.
1 parent 5fc1b35 commit ae848c8

21 files changed

+6929
-3003
lines changed

.github/workflows/publish.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
7777

7878
- name: Send GitHub Action data to a Slack workflow
79-
if: steps.changesets.outputs.published === 'true'
79+
if: steps.changesets.outputs.published == 'true'
8080
uses: slackapi/[email protected]
8181
with:
8282
payload-delimiter: '_'

contributing_docs/testing.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Testing
2+
3+
## Testing Types
4+
5+
You can test types using vitest. It's important to note that testing types does not actually _run_ a test file.
6+
7+
When you test types, these are statically analyzed by the compiler.
8+
9+
Vitest defaults state that all files matching `*.test-d.ts` are considered type-tests
10+
11+
From the vitest docs:
12+
13+
```
14+
Under the hood Vitest calls tsc or vue-tsc, depending on your config, and parses results. Vitest will also print out type errors in your source code, if it finds any. You can disable it with typecheck.ignoreSourceErrors config option.
15+
16+
Keep in mind that Vitest doesn't run these files, they are only statically analyzed by the compiler. Meaning, that if you use a dynamic name or test.each or test.for, the test name will not be evaluated - it will be displayed as is.
17+
```

e2e/davinci-app/components/flow-link.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FlowCollector, InitFlow } from '@forgerock/davinci-client/types';
1+
import type { FlowCollector, InitFlow } from '@forgerock/davinci-client/types';
22

33
export default function flowLinkComponent(
44
formEl: HTMLFormElement,

e2e/davinci-suites/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"main": "src/index.js",
88
"author": "",
99
"license": "ISC",
10+
"nx": {
11+
"implicitDependencies": ["@forgerock/davinci-app", "@forgerock/mock-api-v2"]
12+
},
1013
"repository": {
1114
"type": "git",
1215
"url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git"

e2e/mock-api-v2/tsconfig.app.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
"types": ["node"],
1010
"exactOptionalPropertyTypes": true,
1111
"strictNullChecks": true,
12-
"noErrorTruncation": true
12+
"noErrorTruncation": true,
13+
"skipLibCheck": true,
14+
"plugins": [
15+
{
16+
"name": "@effect/language-service"
17+
}
18+
]
1319
},
1420
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
1521
"include": ["src/**/*.ts"]

package.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@
4141
"@commitlint/cli": "^19.1.0",
4242
"@commitlint/config-conventional": "^19.1.0",
4343
"@commitlint/prompt": "^19.1.0",
44-
"@effect/language-service": "^0.1.0",
45-
"@effect/platform": "^0.58.27",
46-
"@effect/platform-node": "^0.53.26",
47-
"@effect/schema": "^0.68.23",
48-
"@effect/vitest": "^0.6.7",
44+
"@effect/language-service": "^0.2.0",
45+
"@effect/platform": "^0.72.1",
46+
"@effect/platform-node": "^0.68.1",
47+
"@effect/schema": "^0.75.5",
48+
"@effect/vitest": "^0.16.1",
4949
"@forgerock/create-package": "workspace:*",
5050
"@nx/devkit": "20.2.2",
5151
"@nx/eslint": "20.2.2",
@@ -69,14 +69,14 @@
6969
"@typescript-eslint/parser": "7.16.1",
7070
"@typescript-eslint/typescript-estree": "5.59.5",
7171
"@typescript-eslint/utils": "^8.13.0",
72-
"@vitest/coverage-v8": "^1.5.0",
73-
"@vitest/ui": "^1.4.0",
72+
"@vitest/coverage-v8": "^2.1.8",
73+
"@vitest/ui": "^2.1.8",
7474
"conventional-changelog-conventionalcommits": "^7.0.2",
7575
"cz-conventional-changelog": "^3.3.0",
7676
"cz-git": "^1.6.1",
7777
"effect": "^3.5.3",
78-
"effect-http": "^0.73.0",
79-
"effect-http-node": "^0.16.1",
78+
"effect-http": "0.73.0",
79+
"effect-http-node": "0.24.0",
8080
"eslint": "8.57.0",
8181
"eslint-config-prettier": "9.1.0",
8282
"eslint-plugin-import": "2.27.5",
@@ -107,7 +107,7 @@
107107
"vite-plugin-eslint": "^1.8.1",
108108
"vite-plugin-externalize-deps": "^0.8.0",
109109
"vite-tsconfig-paths": "^4.3.2",
110-
"vitest": "^1.4.0",
110+
"vitest": "^2.1.8",
111111
"vitest-canvas-mock": "^0.3.3"
112112
},
113113
"config": {

packages/davinci-client/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"typings": "./dist/index.d.ts",
77
"type": "module",
88
"files": ["dist"],
9+
"sideEffects": ["./src/types.js"],
910
"nx": {
1011
"tags": ["scope:package"]
1112
},
@@ -22,9 +23,6 @@
2223
"@reduxjs/toolkit": "catalog:",
2324
"immer": "catalog:"
2425
},
25-
"devDependencies": {
26-
"vitest": "^1.4.0"
27-
},
2826
"exports": {
2927
".": {
3028
"import": "./dist/index.js",

packages/davinci-client/src/lib/client.store.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
5757
if (!action.action) {
5858
console.error('Missing `argument.action`');
5959
return async function () {
60-
return { error: { message: 'Missing argument.action', type: 'argument_error' } };
60+
return {
61+
error: { message: 'Missing argument.action', type: 'argument_error' },
62+
type: 'internal_error',
63+
};
6164
};
6265
}
6366

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { describe, expectTypeOf, it } from 'vitest';
3+
import type { InitFlow, InternalErrorResponse, FlowNode, Updater } from './client.types.js';
4+
import type { GenericError } from './error.types.js';
5+
import type { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types.js';
6+
7+
describe('Client Types', () => {
8+
it('should allow function returning error', async () => {
9+
// This test isn't excellent but without narrowing inside the function we cant
10+
// narrow the promise type enough without some help on the generic
11+
const withError = async (): Promise<InternalErrorResponse> => {
12+
const result = await Promise.resolve<InternalErrorResponse>({
13+
error: { message: 'Test error', type: 'argument_error' as const },
14+
type: 'internal_error' as const,
15+
});
16+
return result;
17+
};
18+
19+
expectTypeOf(withError()).resolves.toMatchTypeOf<InternalErrorResponse>();
20+
});
21+
it('should allow function returning node types', () => {
22+
const withErrorNode: InitFlow = async () => ({
23+
cache: {
24+
key: 'string',
25+
},
26+
client: {
27+
status: 'error',
28+
},
29+
error: {
30+
type: 'argument_error',
31+
status: 'failure',
32+
message: 'failed',
33+
},
34+
httpStatus: 400,
35+
server: {
36+
status: 'error',
37+
},
38+
status: 'error',
39+
});
40+
41+
const withFailureNode: InitFlow = async () => ({
42+
cache: {
43+
key: '',
44+
},
45+
client: {
46+
status: 'failure',
47+
},
48+
error: {
49+
type: 'state_error',
50+
status: 'failure',
51+
message: 'failed',
52+
},
53+
httpStatus: 404,
54+
server: null,
55+
status: 'failure',
56+
});
57+
58+
const withContinueNode: InitFlow = async () => ({
59+
cache: {
60+
key: 'cachekey',
61+
},
62+
client: {
63+
action: 'action',
64+
collectors: [],
65+
description: 'the description',
66+
name: 'continue_node_name',
67+
status: 'continue',
68+
},
69+
error: null,
70+
httpStatus: 200,
71+
server: {
72+
eventName: 'continue_event',
73+
status: 'continue',
74+
},
75+
status: 'continue',
76+
});
77+
78+
const withStartNode: InitFlow = async () => ({
79+
cache: null,
80+
client: {
81+
status: 'start',
82+
},
83+
error: null,
84+
server: {
85+
status: 'start',
86+
},
87+
status: 'start',
88+
});
89+
90+
const withSuccessNode: InitFlow = async () => ({
91+
cache: {
92+
key: 'key',
93+
},
94+
client: {
95+
authorization: {
96+
code: 'code123412',
97+
state: 'code123213',
98+
},
99+
status: 'success',
100+
},
101+
error: null,
102+
httpStatus: 200,
103+
server: {
104+
eventName: 'success_event',
105+
id: 'theid',
106+
interactionId: '213123',
107+
interactionToken: '123213',
108+
status: 'success',
109+
},
110+
status: 'success',
111+
});
112+
113+
// Test return types
114+
// @ts-expect-error - This is a problem because ErrorResponse does not have a discriminator that separates it from FlowNode (see `error` key in both types.)
115+
expectTypeOf(withErrorNode).returns.resolves.toMatchTypeOf<ErrorNode>();
116+
// @ts-expect-error - This is a problem because ErrorResponse does not have a discriminator that separates it from FlowNode (see `error` key in both types.)
117+
expectTypeOf(withFailureNode).returns.resolves.toMatchTypeOf<FailureNode>();
118+
// @ts-expect-error - This is a problem because ErrorResponse does not have a discriminator that separates it from FlowNode (see `error` key in both types.)
119+
expectTypeOf(withContinueNode).returns.resolves.toMatchTypeOf<ContinueNode>();
120+
// @ts-expect-error - This is a problem because ErrorResponse does not have a discriminator that separates it from FlowNode (see `error` key in both types.)
121+
expectTypeOf(withStartNode).returns.resolves.toMatchTypeOf<StartNode>();
122+
// @ts-expect-error - This is a problem because ErrorResponse does not have a discriminator that separates it from FlowNode (see `error` key in both types.)
123+
expectTypeOf(withSuccessNode).returns.resolves.toMatchTypeOf<SuccessNode>();
124+
125+
// Test that all are valid InitFlow types
126+
expectTypeOf(withErrorNode).toMatchTypeOf<InitFlow>();
127+
expectTypeOf(withFailureNode).toMatchTypeOf<InitFlow>();
128+
expectTypeOf(withContinueNode).toMatchTypeOf<InitFlow>();
129+
expectTypeOf(withStartNode).toMatchTypeOf<InitFlow>();
130+
expectTypeOf(withSuccessNode).toMatchTypeOf<InitFlow>();
131+
});
132+
133+
it('should enforce async function return type', () => {
134+
// @ts-expect-error - Should not allow non-promise return
135+
const invalid: InitFlow = () => ({
136+
cache: {
137+
key: 'key',
138+
},
139+
client: {
140+
action: 'continue',
141+
collectors: [],
142+
status: 'continue',
143+
},
144+
error: null,
145+
httpStatus: 200,
146+
server: {
147+
status: 'continue',
148+
},
149+
status: 'continue',
150+
});
151+
152+
expectTypeOf<InitFlow>().toBeFunction();
153+
154+
// @ts-expect-error - Should not allow non-promise return - we expect this to error
155+
expectTypeOf<InitFlow>().returns.toEqualTypeOf<InitFlow>;
156+
});
157+
});
158+
159+
describe('Updater', () => {
160+
it('should accept string value and optional index', () => {
161+
const updater: Updater = (value: string, index?: number) => {
162+
return {
163+
error: { message: 'Invalid value', code: 'INVALID', type: 'state_error' },
164+
type: 'internal_error',
165+
};
166+
};
167+
168+
expectTypeOf(updater).parameter(0).toBeString();
169+
expectTypeOf(updater).parameter(1).toBeNullable();
170+
expectTypeOf(updater).parameter(1).toBeNullable();
171+
});
172+
173+
it('should return error or null', () => {
174+
const withError: Updater = () => ({
175+
error: { message: 'Invalid value', code: 'INVALID', type: 'state_error' },
176+
type: 'internal_error',
177+
});
178+
179+
const withoutError: Updater = () => null;
180+
181+
expectTypeOf(withError).returns.toMatchTypeOf<{ error: GenericError } | null>();
182+
expectTypeOf(withoutError).returns.toMatchTypeOf<{ error: GenericError } | null>();
183+
184+
// Test both are valid Updater types
185+
expectTypeOf(withError).toMatchTypeOf<Updater>();
186+
expectTypeOf(withoutError).toMatchTypeOf<Updater>();
187+
});
188+
});
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { GenericError } from './error.types';
22
import { ErrorNode, FailureNode, ContinueNode, StartNode, SuccessNode } from './node.types';
33

4-
export type InitFlow =
5-
| (() => Promise<{ error: GenericError }>)
6-
| (() => Promise<ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode>);
4+
export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode;
75

8-
export type Updater = (value: string, index?: number) => { error: GenericError } | null;
6+
export interface InternalErrorResponse {
7+
error: GenericError;
8+
type: 'internal_error';
9+
}
10+
11+
export type InitFlow = () => Promise<FlowNode | InternalErrorResponse>;
12+
13+
export type Updater = (value: string, index?: number) => InternalErrorResponse | null;

0 commit comments

Comments
 (0)