Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/react-type-checks-its-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---

chore(react): `@object-ui/react` type-checks its whole test tree.

No published code moves. The package gains a `tsconfig.test.json` chained from
`type-check`, its 43 code-tier test errors are fixed in the tests themselves,
and the narrow `tsconfig.typetests.json` — the rescue hatch for a package still
in `TEST_DEBT` — is retired now that the full project compiles the same file
(objectui#4040 tranche 4, under objectui#4291's ratchet).
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"scripts": {
"build": "tsc",
"test": "vitest run",
"type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json",
"type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
"lint": "eslint ."
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
import { render } from '@testing-library/react';
import React from 'react';
import { ComponentRegistry } from '@object-ui/core';
Expand All @@ -19,7 +19,12 @@ const PassthroughDiv: React.FC<any> = (props) => {
};

describe('SchemaRenderer — dev-mode validation', () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
// `ReturnType<typeof vi.spyOn>` resolves to the un-instantiated
// `MockInstance<Procedure | Constructable>` union, whose `mock.calls` degrades
// to an implicitly-`any` element type — so every `.filter((call) => ...)`
// below was unchecked (objectui#4040). Naming the spied signature keeps
// `calls` a real tuple array.
let warnSpy: MockInstance<typeof console.warn>;

beforeEach(() => {
ComponentRegistry.register('valid-host', PassthroughDiv);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ afterEach(() => {
});

// Helper: a component that calls useSchemaContext (mimics plugin pattern)
const ContextConsumer: React.FC<any> = ({ schema }) => {
const ContextConsumer: React.FC<any> = () => {
const { dataSource } = useSchemaContext();
return <div data-testid="ctx-consumer">dataSource: {JSON.stringify(dataSource)}</div>;
};
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/__tests__/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ const lightTheme: Theme = {
error: '#EF4444',
},
typography: {
// `fontFamily.base` is the only surviving typography key: `fontSize` (and
// `fontWeight` / `lineHeight` / `letterSpacing`) became tombstones in
// @objectstack/spec 17.0.0-rc.3, prescribing `theme.customVars` instead
// (objectstack#5021). The fixture carried `fontSize: { base: '1rem' }`
// until this package started type-checking its tests (objectui#4040); no
// assertion in this file ever read it.
fontFamily: { base: 'Inter, sans-serif' },
fontSize: { base: '1rem' },
},
borderRadius: { base: '0.25rem', lg: '0.5rem' },
};
Expand Down
5 changes: 4 additions & 1 deletion packages/react/src/__tests__/useDensityMode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ describe('useDensityMode persistence', () => {
const { result, rerender } = renderHook(
({ initial }: { initial: 'compact' | 'comfortable' | 'spacious' }) =>
useDensityMode(initial, { onChange }),
{ initialProps: { initial: 'compact' as const } }
// No `as const` on `initial`: it would pin the hook's Props to the
// literal `'compact'` and make the `rerender` below — the whole point of
// the case — a type error rather than an external change.
{ initialProps: { initial: 'compact' } }
);

expect(result.current.mode).toBe('compact');
Expand Down
21 changes: 11 additions & 10 deletions packages/react/src/context/__tests__/ActionContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('ActionContext', () => {

it('returns true with provider', () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: {} }, children);
React.createElement(ActionProvider, { context: {}, children });

const { result } = renderHook(() => useHasActionProvider(), { wrapper });

Expand All @@ -42,7 +42,7 @@ describe('ActionContext', () => {
describe('ActionProvider basic', () => {
it('children can access execute function', () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: {} }, children);
React.createElement(ActionProvider, { context: {}, children });

const { result } = renderHook(() => useAction(), { wrapper });

Expand All @@ -58,7 +58,7 @@ describe('ActionContext', () => {
describe('ActionProvider execute', () => {
it('executes a toast action and returns result', async () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: {} }, children);
React.createElement(ActionProvider, { context: {}, children });

const { result } = renderHook(() => useAction(), { wrapper });

Expand Down Expand Up @@ -86,7 +86,7 @@ describe('ActionContext', () => {
describe('ActionProvider execute error', () => {
it('executes an action that fails and sets error state', async () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: {} }, children);
React.createElement(ActionProvider, { context: {}, children });

const { result } = renderHook(() => useAction(), { wrapper });

Expand All @@ -110,7 +110,7 @@ describe('ActionContext', () => {
describe('ActionProvider executeChain', () => {
it('executes a chain of actions', async () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: {} }, children);
React.createElement(ActionProvider, { context: {}, children });

const { result } = renderHook(() => useAction(), { wrapper });

Expand All @@ -135,7 +135,7 @@ describe('ActionContext', () => {
describe('ActionProvider updateContext', () => {
it('updateContext is callable', () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: {} }, children);
React.createElement(ActionProvider, { context: {}, children });

const { result } = renderHook(() => useAction(), { wrapper });

Expand All @@ -151,11 +151,12 @@ describe('ActionContext', () => {
const onNavigate = vi.fn();

const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(
ActionProvider,
{ context: {}, onToast, onNavigate },
React.createElement(ActionProvider, {
context: {},
onToast,
onNavigate,
children,
);
});

const { result } = renderHook(() => useAction(), { wrapper });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ describe('the ConflictResolutionStrategy rename is load-bearing', () => {

/* -------------------------------------------------------------------------- */
/* Compile-time pins. A violation is a `tsc` error, not a runtime failure. */
/* Compiled by this package's `tsconfig.typetests.json` (objectui#3181). */
/* Compiled by this package's `tsconfig.test.json` (objectui#3181): the narrow */
/* `tsconfig.typetests.json` that used to name this one file was the rescue */
/* hatch for a package still in TEST_DEBT, and it was retired with the debt */
/* (objectui#4040 / objectui#4291). */
/* -------------------------------------------------------------------------- */

type Assert<T extends true> = T;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const SELF_ID = 'user-self';

function withProvider(providerContext: any) {
return ({ children }: { children: React.ReactNode }) =>
React.createElement(ActionProvider, { context: providerContext }, children);
React.createElement(ActionProvider, { context: providerContext, children });
}

describe('useActionEngine — shared ActionProvider runner', () => {
Expand Down
19 changes: 18 additions & 1 deletion packages/react/src/hooks/__tests__/useActionEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@
import { describe, it, expect, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { ActionLocationSchema } from '@objectstack/spec/ui';
import type { ActionDef } from '@object-ui/core';
import { useActionEngine } from '../useActionEngine';

describe('useActionEngine', () => {
const sampleActions = [
// Annotated rather than inferred: without it every `useActionEngine({ actions:
// sampleActions })` below re-reported the same widening as a call-site error
// once this package started type-checking its tests (objectui#4040), and the
// one entry that is deliberately off-spec could not be told apart from an
// accident.
const sampleActions: ActionDef[] = [
{
name: 'mark_complete',
type: 'script',
target: 'true',
locations: ['list_toolbar', 'record_header'],
// @ts-expect-error `bulkEnabled` is a retired key on `ActionDef`, kept
// here as the only input that can prove `getBulkActions()` does not
// harvest it (see the inverse test below). The directive is
// self-invalidating: un-retiring the key makes it unused, i.e. TS2578.
bulkEnabled: true,
},
{
Expand All @@ -40,7 +50,14 @@ describe('useActionEngine', () => {
name: 'global_search',
type: 'script',
target: 'true',
// @ts-expect-error `global_nav` is no longer an `ActionLocation` — that
// rejection IS the premise of the inverse test below, so the directive
// pins it: were the location ever re-admitted to the spec, this line
// would stop erroring and TS2578 would report the now-unused directive.
locations: ['global_nav'],
// @ts-expect-error `shortcut` is retired on `ActionDef` too, and for the
// same reason as `bulkEnabled` above: the inverse test needs metadata
// that still declares one.
shortcut: 'ctrl+k',
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ import { I18nProvider, useObjectTranslation } from '@object-ui/i18n';
import { useActionTextLocalizer } from '../useActionTextLocalizer';

const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(
I18nProvider,
{ config: { defaultLanguage: 'en', detectBrowserLanguage: false } },
React.createElement(I18nProvider, {
config: { defaultLanguage: 'en', detectBrowserLanguage: false },
children,
);
});

/** The authored action, exactly as `src/actions/lead.actions.ts` declares it. */
const CONVERT_LEAD = {
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/__tests__/useExpression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('useRowPredicate (canonical CEL row predicate — issue #1584)', () =>

it('merges the ambient predicate scope (features/user)', () => {
const wrapper = ({ children }: { children: React.ReactNode }) =>
createElement(PredicateScopeProvider, { scope: { features: { canEdit: true } } }, children);
createElement(PredicateScopeProvider, { scope: { features: { canEdit: true } }, children });
expect(
renderHook(() => useRowPredicate('features.canEdit == true', { id: '1' }), { wrapper }).result.current,
).toBe(true);
Expand Down Expand Up @@ -209,7 +209,7 @@ describe('usePredicateRecordContext — no row binds NOTHING (objectui#4075)', (
// `{ ...scope, ...context }`, so an empty-row bag would shadow the scope's
// `record` with `{}` and a correctly-authored predicate would read false.
const wrapper = ({ children }: { children: React.ReactNode }) =>
createElement(PredicateScopeProvider, { scope: { record: { status: 'pending' } } }, children);
createElement(PredicateScopeProvider, { scope: { record: { status: 'pending' } }, children });
const { result } = renderHook(
() => useCondition('${record.status === "pending"}', usePredicateRecordContext(undefined)),
{ wrapper },
Expand Down
24 changes: 21 additions & 3 deletions packages/react/src/hooks/__tests__/useRecordSearch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('useRecordSearch', () => {
it('discards stale results when query changes mid-flight', async () => {
const accountResolvers: Array<(v: any) => void> = [];
const ds = {
find: vi.fn((objectName: string, q: any) => {
find: vi.fn((objectName: string, _query: any) => {
if (objectName === 'account') {
return new Promise((resolve) => {
accountResolvers.push((v) => resolve(v));
Expand Down Expand Up @@ -301,7 +301,17 @@ describe('useRecordSearch', () => {
function makeSearchDataSource(hits: any[]) {
return {
find: vi.fn(async () => ({ data: [] })),
searchAll: vi.fn(async () => ({ query: 'wayne', hits })),
// The parameters are declared even though the body ignores them: the
// hook calls `searchAll(term, { objects, limit })`, and a zero-arity
// `vi.fn` types `mock.calls[0]` as the empty tuple `[]`, so every
// `const [term, opts] = ...` below read `undefined` as far as the
// compiler was concerned (objectui#4040).
searchAll: vi.fn(
async (
_term: string,
_options: { objects?: string[]; limit?: number },
) => ({ query: 'wayne', hits }),
),
};
}

Expand Down Expand Up @@ -420,7 +430,15 @@ describe('useRecordSearch', () => {
}));
const ds = {
find: vi.fn(),
searchAll: vi.fn(async () => ({ hits: [] })),
// Parameters declared for the same reason as `makeSearchDataSource`
// above — a zero-arity `vi.fn` makes `mock.calls[0]` the empty tuple,
// and the `opts.objects` assertion below is the whole regression.
searchAll: vi.fn(
async (
_term: string,
_options: { objects?: string[]; limit?: number },
) => ({ hits: [] }),
),
};

renderHook(() =>
Expand Down
56 changes: 36 additions & 20 deletions packages/react/src/spec-bridge/__tests__/SpecBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,13 @@ describe('SpecBridge', () => {
type: 'badge',
pinned: 'left',
summary: { type: 'count' },
link: { href: '/status' },
action: { type: 'navigate' },
// `ListColumn.link` is a BOOLEAN in @objectstack/spec and
// `action` is the NAME of an action — not the `{ href }` /
// `{ type }` envelopes this fixture used to author. The bridge
// forwards both keys verbatim, so the old assertions passed on
// any value at all and proved only pass-through (objectui#4040).
link: true,
action: 'navigate',
},
],
},
Expand All @@ -152,45 +157,56 @@ describe('SpecBridge', () => {
expect(col.type).toBe('badge');
expect(col.pinned).toBe('left');
expect(col.summary).toEqual({ type: 'count' });
expect(col.link).toEqual({ href: '/status' });
expect(col.action).toEqual({ type: 'navigate' });
expect(col.link).toBe(true);
expect(col.action).toBe('navigate');
});

it('maps rowHeight to density', () => {
// All FIVE spellings `RowHeightSchema` admits, and only those. The fixture
// used to author `comfortable` / `spacious` / `small` as well — three values
// no spec-valid list view can carry — and it type-checked against nothing,
// so `mapDensity`'s branches for them read as live capability. What a caller
// can actually hand this bridge is the enum below (objectui#4040).
it('maps every spec rowHeight to a density', () => {
const compact = bridgeListView({ rowHeight: 'compact' }, {});
expect(compact.density).toBe('compact');

const comfortable = bridgeListView({ rowHeight: 'comfortable' }, {});
expect(comfortable.density).toBe('comfortable');

const spacious = bridgeListView({ rowHeight: 'spacious' }, {});
expect(spacious.density).toBe('spacious');

const small = bridgeListView({ rowHeight: 'small' }, {});
expect(small.density).toBe('compact');

const short = bridgeListView({ rowHeight: 'short' }, {});
expect(short.density).toBe('compact');

const medium = bridgeListView({ rowHeight: 'medium' }, {});
expect(medium.density).toBe('comfortable');

const tall = bridgeListView({ rowHeight: 'tall' }, {});
expect(tall.density).toBe('spacious');

const extraTall = bridgeListView({ rowHeight: 'extra_tall' }, {});
expect(extraTall.density).toBe('spacious');
});

it('includes optional list properties', () => {
const node = bridgeListView(
{
sort: { field: 'name', direction: 'asc' },
filter: { status: 'active' },
grouping: { field: 'region' },
// Spec spellings throughout: `sort` is a list of `{ field, order }`
// (not a single `{ field, direction }`), `filter` is a list of
// `{ field, operator, value }` predicates (not a field→value map),
// and `grouping` wraps its levels in `fields`. The bridge forwards
// each key verbatim, so the previous fixture's dialect round-tripped
// and the assertions passed while describing metadata that cannot be
// authored (objectui#4040).
sort: [{ field: 'name', order: 'asc' }],
filter: [{ field: 'status', operator: 'equals', value: 'active' }],
grouping: { fields: [{ field: 'region' }] },
rowColor: { field: 'priority' },
searchableFields: ['name', 'email'],
},
{},
);

expect(node.sort).toEqual({ field: 'name', direction: 'asc' });
expect(node.filter).toEqual({ status: 'active' });
expect(node.grouping).toEqual({ field: 'region' });
expect(node.sort).toEqual([{ field: 'name', order: 'asc' }]);
expect(node.filter).toEqual([
{ field: 'status', operator: 'equals', value: 'active' },
]);
expect(node.grouping).toEqual({ fields: [{ field: 'region' }] });
expect(node.rowColor).toEqual({ field: 'priority' });
expect(node.searchableFields).toEqual(['name', 'email']);
});
Expand Down
Loading
Loading