Establishes the load-bearing type surface for the Backstage rebuild: - src/admin/resource-types.ts — discriminated unions for Field, Column, Filter, Action, plus the top-level Resource and ResourceGroup. Strict per the maintainability bar: a config object missing a required key fails TypeScript. - src/admin/validate.ts — validateForResource() derives validation from the field definitions (required, maxLength, multi-text min/max, number bounds, date parse, visibleWhen-aware). - tests/admin-validate.test.ts — 8 cases locking the validator API: required, maxLength, visibleWhen skip & reveal, multi-text bounds, number bounds, all-valid, form-null short-circuit. No consumers yet. Next commit pulls these into admin.css and the shared layout components. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { validateForResource } from '../src/admin/validate';
|
|
import type { Resource } from '../src/admin/resource-types';
|
|
|
|
// Minimal resource fixture covering the field kinds the validator handles.
|
|
const resource: Resource = {
|
|
key: 'fixtures',
|
|
label: 'Fixture',
|
|
pluralLabel: 'Fixtures',
|
|
singularLabel: 'Fixture',
|
|
groupKey: 'system',
|
|
list: { queryFn: () => [], columns: [] },
|
|
ops: {},
|
|
form: {
|
|
fields: [
|
|
{ key: 'title', label: 'Title', kind: 'text', required: true, maxLength: 5 },
|
|
{ key: 'kind', label: 'Kind', kind: 'select', options: [{ value: 'a', label: 'A' }] },
|
|
{
|
|
key: 'options',
|
|
label: 'Options',
|
|
kind: 'multi-text',
|
|
minItems: 2,
|
|
maxItems: 4,
|
|
},
|
|
{ key: 'count', label: 'Count', kind: 'number', min: 0, max: 10 },
|
|
{
|
|
key: 'extras',
|
|
label: 'Extras',
|
|
kind: 'text',
|
|
// Only visible when kind === 'a'
|
|
visibleWhen: (ctx) => ctx.formValues.kind === 'a',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const baseArgs = { resource, item: null, actingUserId: 1 };
|
|
|
|
describe('validateForResource', () => {
|
|
it('reports required fields that are empty', () => {
|
|
const errors = validateForResource({ ...baseArgs, data: {} });
|
|
expect(errors.title).toBe('Title is required');
|
|
});
|
|
|
|
it('reports maxLength violations on text fields', () => {
|
|
const errors = validateForResource({ ...baseArgs, data: { title: 'too long' } });
|
|
expect(errors.title).toMatch(/5 characters or fewer/);
|
|
});
|
|
|
|
it('skips fields hidden by visibleWhen', () => {
|
|
// kind = 'b' hides the 'extras' required field — no error expected
|
|
const errors = validateForResource({
|
|
...baseArgs,
|
|
data: { title: 'ok', kind: 'b' },
|
|
});
|
|
expect(errors.extras).toBeUndefined();
|
|
});
|
|
|
|
it('enforces visibleWhen-revealed required fields', () => {
|
|
const errors = validateForResource({
|
|
...baseArgs,
|
|
data: { title: 'ok', kind: 'a' },
|
|
});
|
|
expect(errors.extras).toBe('Extras is required');
|
|
});
|
|
|
|
it('enforces multi-text min/max items', () => {
|
|
const tooFew = validateForResource({
|
|
...baseArgs,
|
|
data: { title: 'ok', options: ['only-one'] },
|
|
});
|
|
expect(tooFew.options).toMatch(/at least 2/);
|
|
|
|
const tooMany = validateForResource({
|
|
...baseArgs,
|
|
data: { title: 'ok', options: ['1', '2', '3', '4', '5'] },
|
|
});
|
|
expect(tooMany.options).toMatch(/at most 4/);
|
|
});
|
|
|
|
it('enforces number min/max', () => {
|
|
const tooHigh = validateForResource({
|
|
...baseArgs,
|
|
data: { title: 'ok', count: 99 },
|
|
});
|
|
expect(tooHigh.count).toMatch(/no more than 10/);
|
|
});
|
|
|
|
it('returns no errors when every field is valid', () => {
|
|
const errors = validateForResource({
|
|
...baseArgs,
|
|
data: {
|
|
title: 'ok',
|
|
kind: 'a',
|
|
options: ['x', 'y'],
|
|
count: 5,
|
|
extras: 'fine',
|
|
},
|
|
});
|
|
expect(errors).toEqual({});
|
|
});
|
|
|
|
it('treats form: null as always valid', () => {
|
|
const readOnly: Resource = { ...resource, form: null };
|
|
const errors = validateForResource({
|
|
...baseArgs,
|
|
resource: readOnly,
|
|
data: {},
|
|
});
|
|
expect(errors).toEqual({});
|
|
});
|
|
});
|