angular/packages/forms/signals/test/node/logic_node.spec.ts
Leon Senft 46dbd18566 refactor(forms): remove customError()
Remove the `customError` function and `CustomValidationError` type.

These were made obsolete by support for returning plain object literals
as custom errors.

This also catches few `field` properties that were missed in the
renaming to `fieldTree`.
2026-01-07 15:07:30 -05:00

428 lines
14 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {computed, signal} from '@angular/core';
import {FieldContext} from '../../public_api';
import {DYNAMIC} from '../../src/schema/logic';
import {LogicNodeBuilder} from '../../src/schema/logic_node';
const fakeFieldContext: FieldContext<unknown> = {
fieldTreeOf: () => undefined!,
stateOf: () =>
({
context: undefined,
structure: {pathKeys: () => [], parent: undefined},
}) as any,
valueOf: () => undefined!,
fieldTree: undefined!,
state: undefined!,
value: undefined!,
pathKeys: computed(() => []),
};
describe('LogicNodeBuilder', () => {
it('should build logic', () => {
// (p) => {
// validate(p, () => ({kind: 'root-err'}));
// };
const builder = LogicNodeBuilder.newRoot();
builder.addSyncErrorRule(() => [{kind: 'root-err', fieldTree: undefined as any}]);
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'root-err', fieldTree: undefined as any},
]);
});
it('should build child logic', () => {
// (p) => {
// validate(p.a, () => ({kind: 'child-err'}));
// };
const builder = LogicNodeBuilder.newRoot();
builder.getChild('a').addSyncErrorRule(() => [{kind: 'root-err', fieldTree: undefined as any}]);
const logicNode = builder.build();
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'root-err', fieldTree: undefined as any},
]);
});
it('should build merged logic', () => {
// (p) => {
// validate(p, () => ({kind: 'err-1'}));
// validate(p, () => ({kind: 'err-2'}));
// };
const builder = LogicNodeBuilder.newRoot();
builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
const builder2 = LogicNodeBuilder.newRoot();
builder2.addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
builder.mergeIn(builder2);
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
{kind: 'err-2', fieldTree: undefined as any},
]);
});
it('should build merged child logic', () => {
// (p) => {
// validate(p.a, () => ({kind: 'err-1'}));
// validate(p.a, () => ({kind: 'err-2'}));
// };
const builder = LogicNodeBuilder.newRoot();
builder.getChild('a').addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
const builder2 = LogicNodeBuilder.newRoot();
builder2.getChild('a').addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
builder.mergeIn(builder2);
const logicNode = builder.build();
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
{kind: 'err-2', fieldTree: undefined as any},
]);
});
it('should build logic with predicate', () => {
// (p) => {
// applyWhen(p, pred, (p) => {
// validate(p, () => ({kind: 'err-1'}));
// });
// }
const builder = LogicNodeBuilder.newRoot();
const pred = signal(true);
const builder2 = LogicNodeBuilder.newRoot();
builder2.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
pred.set(false);
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
});
it('should apply predicate to merged in logic', () => {
// (p) => {
// applyWhen(p, pred, (p) => {
// apply(p, (p) => {
// validate(p, () => ({kind: 'err-1'}));
// });
// });
// }
const builder = LogicNodeBuilder.newRoot();
const pred = signal(true);
const builder2 = LogicNodeBuilder.newRoot();
const builder3 = LogicNodeBuilder.newRoot();
builder3.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder2.mergeIn(builder3);
builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
pred.set(false);
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
});
it('should apply predicate to merged in child logic', () => {
// (p) => {
// applyWhen(p, pred, (p) => {
// apply(p, (p) => {
// validate(p.a, () => ({kind: 'err-1'}));
// });
// });
// }
const builder = LogicNodeBuilder.newRoot();
const pred = signal(true);
const builder2 = LogicNodeBuilder.newRoot();
const builder3 = LogicNodeBuilder.newRoot();
builder3.getChild('a').addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder2.mergeIn(builder3);
builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
pred.set(false);
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
});
it('should combine predicates', () => {
// (p) => {
// applyWhen(p, pred, (p) => {
// applyWhen(p.a, pred2, (a) => {
// validate(a, () => ({kind: 'err-1'}));
// });
// });
// }
const builder = LogicNodeBuilder.newRoot();
const pred = signal(true);
const builder2 = LogicNodeBuilder.newRoot();
const pred2 = signal(true);
const builder3 = LogicNodeBuilder.newRoot();
builder3.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder2.getChild('a').mergeIn(builder3, {fn: () => pred2(), path: undefined!});
builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
pred.set(false);
pred2.set(true);
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
pred.set(true);
pred2.set(false);
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
});
it('should propagate predicates through deep application', () => {
// (p) => {
// applyWhen(p, pred, (p) => {
// validate(p.a.b, () => ({kind: 'err-1'}));
// applyWhen(p.a, pred2, (a) => {
// validate(a.b, () => ({kind: 'err-2'}));
// applyWhen(a.b, pred3, (b) => {
// validate(b, () => ({kind: 'err-3'}));
// });
// });
// });
// }
const builder = LogicNodeBuilder.newRoot();
const pred = signal(true);
const builder2 = LogicNodeBuilder.newRoot();
builder2
.getChild('a')
.getChild('b')
.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
const pred2 = signal(true);
const builder3 = LogicNodeBuilder.newRoot();
builder3.getChild('b').addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
const pred3 = signal(true);
const builder4 = LogicNodeBuilder.newRoot();
builder4.addSyncErrorRule(() => [{kind: 'err-3', fieldTree: undefined as any}]);
builder3.getChild('b').mergeIn(builder4, {fn: () => pred3(), path: undefined!});
builder2.getChild('a').mergeIn(builder3, {fn: () => pred2(), path: undefined!});
builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(
logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
{kind: 'err-2', fieldTree: undefined as any},
{kind: 'err-3', fieldTree: undefined as any},
]);
pred.set(true);
pred2.set(true);
pred3.set(false);
expect(
logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
{kind: 'err-2', fieldTree: undefined as any},
]);
pred.set(true);
pred2.set(false);
pred3.set(true);
expect(
logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);
pred.set(false);
pred2.set(true);
pred3.set(true);
expect(
logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
).toEqual([]);
});
it('should propagate predicates through deep child access', () => {
// (p) => {
// applyWhen(p, pred, (p) => {
// applyEach(p.items, (i) => {
// validate(i.last, () => ({kind: 'err-1'}));
// });
// });
// };
const builder = LogicNodeBuilder.newRoot();
const pred = signal(true);
const builder2 = LogicNodeBuilder.newRoot();
const builder3 = LogicNodeBuilder.newRoot();
builder3
.getChild('last')
.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder2.getChild('items').getChild(DYNAMIC).mergeIn(builder3);
builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(
logicNode
.getChild('items')
.getChild(DYNAMIC)
.getChild('last')
.logic.syncErrors.compute(fakeFieldContext),
).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);
pred.set(false);
expect(
logicNode
.getChild('items')
.getChild(DYNAMIC)
.getChild('last')
.logic.syncErrors.compute(fakeFieldContext),
).toEqual([]);
});
it('should preserve ordering across merges', () => {
// (p) => {
// validate(p, () => ({kind: 'err-1'}));
// apply(p, (p) => {
// validate(p, () => ({kind: 'err-2'}));
// })
// validate(p, () => ({kind: 'err-3'}));
// };
const builder = LogicNodeBuilder.newRoot();
builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
const builder2 = LogicNodeBuilder.newRoot();
builder2.addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
builder.mergeIn(builder2);
builder.addSyncErrorRule(() => [{kind: 'err-3', fieldTree: undefined as any}]);
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
{kind: 'err-2', fieldTree: undefined as any},
{kind: 'err-3', fieldTree: undefined as any},
]);
});
it('should preserve child ordering across merges', () => {
// (p) => {
// validate(p.a, () => ({kind: 'err-1'}));
// apply(p, (p) => {
// validate(p.a, () => ({kind: 'err-2'}));
// })
// validate(p.a, () => ({kind: 'err-3'}));
// };
const builder = LogicNodeBuilder.newRoot();
builder.getChild('a').addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
const builder2 = LogicNodeBuilder.newRoot();
builder2.getChild('a').addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
builder.mergeIn(builder2);
builder.getChild('a').addSyncErrorRule(() => [{kind: 'err-3', fieldTree: undefined as any}]);
const logicNode = builder.build();
expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
{kind: 'err-2', fieldTree: undefined as any},
{kind: 'err-3', fieldTree: undefined as any},
]);
});
it('should support circular logic structures', () => {
// const s = schema((p) => {
// validate(p, () => ({kind: 'err-1'})),
// apply(p.next, s);
// }));
const builder = LogicNodeBuilder.newRoot();
builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder.getChild('next').mergeIn(builder);
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
expect(logicNode.getChild('next').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
expect(
logicNode.getChild('next').getChild('next').logic.syncErrors.compute(fakeFieldContext),
).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);
});
it('should support circular logic structures with predicate', () => {
// const s = schema((p) => {
// validate(p, () => ({kind: 'err-1'})),
// applyWhen(p.next, pred, s);
// }));
const pred = signal(true);
const builder = LogicNodeBuilder.newRoot();
builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
builder.getChild('next').mergeIn(builder, {fn: () => pred(), path: undefined!});
const logicNode = builder.build();
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
expect(logicNode.getChild('next').logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
expect(
logicNode.getChild('next').getChild('next').logic.syncErrors.compute(fakeFieldContext),
).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);
// TODO: test that verifies that the same predicate can resolve with a different field context
// on `.next` vs on `.next.next`
pred.set(false);
expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
{kind: 'err-1', fieldTree: undefined as any},
]);
expect(logicNode.getChild('next').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
expect(
logicNode.getChild('next').getChild('next').logic.syncErrors.compute(fakeFieldContext),
).toEqual([]);
});
});