packages/__tests__/src/validation-html/subscribers/validation-result-presenter-service.spec.ts
import { Registration, toArray, newInstanceForScope, DI, resolve, IContainer } from '@aurelia/kernel';
import { IPlatform, CustomElement, customElement, Aurelia } from '@aurelia/runtime-html';
import { PLATFORM, assert, ISpy, TestContext, createSpy, getVisibleText } from '@aurelia/testing';
import {
IValidationRules,
ValidationResult,
} from '@aurelia/validation';
import {
IValidationController,
ValidationController,
ValidationHtmlConfiguration,
ValidationResultsSubscriber,
ValidationResultPresenterService,
IValidationResultPresenterService as $IValidationResultPresenterService,
} from '@aurelia/validation-html';
import { Person } from '../../validation/_test-resources.js';
import { TestFunction, TestExecutionContext, ToNumberValueConverter, createSpecFunction } from '../../util.js';
describe('validation-html/subscribers/validation-result-presenter-service.spec.ts', function () {
describe('validation-result-presenter-service', function () {
const IValidationResultPresenterService = DI.createInterface<$IValidationResultPresenterService>('ValidationResultPresenterService');
class App {
public person: Person = new Person((void 0)!, (void 0)!);
public controllerValidateSpy: ISpy;
public readonly platform: IPlatform = resolve(IPlatform);
public controller: ValidationController = resolve(newInstanceForScope(IValidationController)) as ValidationController;
public presenterService: ValidationResultPresenterService = resolve(IValidationResultPresenterService) as ValidationResultPresenterService;
private readonly validationRules: IValidationRules = resolve(IValidationRules);
public constructor() {
this.controllerValidateSpy = createSpy(this.controller, 'validate', true);
this.controller.addSubscriber(this.presenterService);
this.validationRules
.on(this.person)
.ensure('name')
.displayName('Name')
.required()
.ensure('age')
.displayName('Age')
.required()
.satisfies((age: any) => age % 3 === 0 && age % 5 === 0)
.withMessage('${$displayName} is not fizbaz');
}
public unbinding() {
this.validationRules.off();
// mandatory cleanup in root
this.controller.removeSubscriber(this.presenterService);
this.controller.reset();
}
public dispose() {
const controller = this.controller;
assert.equal(controller.results.length, 0, 'the result should have been removed');
assert.equal(controller.bindings.size, 0, 'the bindings should have been removed');
assert.equal(controller.objects.size, 0, 'the objects should have been removed');
assert.equal(controller.subscribers.size, 0, 'the subscribers should have been removed');
}
}
interface TestSetupContext {
template: string;
presenterServiceFactory?: (container: IContainer) => $IValidationResultPresenterService;
}
async function runTest(
testFunction: TestFunction<TestExecutionContext<App>>,
{ template, presenterServiceFactory }: TestSetupContext
) {
const ctx = TestContext.create();
const container = ctx.container;
const host = ctx.createElement('app');
ctx.doc.body.appendChild(host);
const au = new Aurelia(container);
await au
.register(
Registration.instance(IPlatform, PLATFORM),
ValidationHtmlConfiguration,
ToNumberValueConverter,
CustomValidationContainer,
Registration.cachedCallback(
IValidationResultPresenterService,
(x) => presenterServiceFactory?.(x) ?? x.get($IValidationResultPresenterService)
),
)
.app({
host,
component: CustomElement.define({ name: 'app', template }, App)
})
.start();
const app = au.root.controller.viewModel as App;
await testFunction({ app, host, container, platform: app.platform, ctx });
await au.stop();
ctx.doc.body.removeChild(host);
au.dispose();
}
const $it = createSpecFunction(runTest);
function getResult(args: unknown) { return (args as ValidationResult[]).flatMap((arg) => [arg.valid, arg.message]); }
async function assertEventHandler(
target: HTMLElement,
platform: IPlatform,
controllerValidateSpy: ISpy,
handleValidationEventSpy: ISpy,
ctx: TestContext,
event: string = 'focusout',
) {
handleValidationEventSpy.calls.splice(0);
controllerValidateSpy.calls.splice(0);
target.dispatchEvent(new ctx.Event(event));
await platform.domQueue.yield();
assert.equal(controllerValidateSpy.calls.length, 1, 'incorrect #calls for validate');
assert.equal(handleValidationEventSpy.calls.length, 1, 'incorrect #calls for handleValidationEvent');
}
function assertSubscriber(controller: ValidationController, subscriber: ValidationResultPresenterService) {
const subscribers = (controller['subscribers'] as Set<ValidationResultsSubscriber>);
assert.equal((subscribers).has(subscriber), true);
}
class CustomPresenterService extends ValidationResultPresenterService {
private static readonly markerProperty: string = 'validation-result-id';
public getValidationMessageContainer(target: Element) {
return target.parentElement.shadowRoot.querySelector('small.validation-placeholder');
}
public showResults(messageContainer: HTMLElement, results: ValidationResult[]) {
messageContainer.append(
...results.reduce((acc: Node[], result) => {
if (!result.valid) {
const text = PLATFORM.document.createTextNode(result.message) as Node;
Reflect.set(text, CustomPresenterService.markerProperty, result.id);
acc.push(text);
}
return acc;
}, [])
);
}
public removeResults(messageContainer: Element, results: ValidationResult[]) {
const children = toArray(messageContainer.childNodes);
for (const result of results) {
if (!result.valid) {
children.find((child) => Reflect.get(child, CustomPresenterService.markerProperty) === result.id)?.remove();
}
}
}
}
@customElement({
name: 'custom-validation-container',
template: `<au-slot></au-slot><small class='validation-placeholder'></small>`,
shadowOptions: { mode: 'open' }
})
class CustomValidationContainer { }
$it('shows the errors for the associated validation targets',
async function ({ host, platform, app, ctx }) {
const div1 = host.querySelector('div');
const div2 = host.querySelector('div:nth-of-type(2)');
const nameError = 'Name is required.';
const ageRequiredError = 'Age is required.';
const ageFizbazError = 'Age is not fizbaz';
const controller = app.controller;
const presenterService = app.presenterService;
assertSubscriber(controller, presenterService);
assertSubscriber(controller, presenterService);
const input1: HTMLInputElement = div1.querySelector('input#target1');
const input2: HTMLInputElement = div2.querySelector('input#target2');
const controllerSpy = app.controllerValidateSpy;
const spy = createSpy(presenterService, 'handleValidationEvent', true);
const addSpy = createSpy(presenterService, 'add', true);
const removeSpy = createSpy(presenterService, 'remove', true);
const showResultsSpy = createSpy(presenterService, 'showResults', true);
const removeResultsSpy = createSpy(presenterService, 'removeResults', true);
await assertEventHandler(input1, platform, controllerSpy, spy, ctx);
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
let addArgs = addSpy.calls;
let removeArgs = removeSpy.calls;
let showResultsArgs = showResultsSpy.calls;
let removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 0);
assert.equal(addArgs.length, 2);
assert.equal(removeResultsArgs.length, 0);
assert.equal(showResultsArgs.length, 2);
assert.deepStrictEqual([addArgs[0][0], ...getResult(addArgs[0][1])], [input1, false, nameError]);
assert.deepStrictEqual([addArgs[1][0], ...getResult(addArgs[1][1])], [input2, false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual([(showResultsArgs[0][0] as HTMLElement).tagName, ...getResult(showResultsArgs[0][1])], ['DIV', false, nameError]);
assert.deepStrictEqual([(showResultsArgs[1][0] as HTMLElement).tagName, ...getResult(showResultsArgs[1][1])], ['DIV', false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual(toArray(div1.querySelectorAll('span')).map((el) => getVisibleText(el, true)), [nameError]);
assert.deepStrictEqual(toArray(div2.querySelectorAll('span')).map((el) => getVisibleText(el, true)), [ageRequiredError, ageFizbazError]);
addSpy.reset();
removeSpy.reset();
showResultsSpy.reset();
removeResultsSpy.reset();
input2.value = '22';
input2.dispatchEvent(new ctx.Event('change'));
await platform.domQueue.yield();
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
addArgs = addSpy.calls;
removeArgs = removeSpy.calls;
showResultsArgs = showResultsSpy.calls;
removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 1);
assert.equal(addArgs.length, 1);
assert.equal(removeResultsArgs.length, 1);
assert.equal(showResultsArgs.length, 1);
assert.deepStrictEqual([removeArgs[0][0], ...getResult(removeArgs[0][1])], [input2, false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual([addArgs[0][0], ...getResult(addArgs[0][1])], [input2, true, undefined, false, ageFizbazError]);
assert.deepStrictEqual([(removeResultsArgs[0][0] as HTMLElement).tagName, ...getResult(removeResultsArgs[0][1])], ['DIV', false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual([(showResultsArgs[0][0] as HTMLElement).tagName, ...getResult(showResultsArgs[0][1])], ['DIV', true, undefined, false, ageFizbazError]);
assert.deepStrictEqual(toArray(div1.querySelectorAll('span')).map((el) => getVisibleText(el, true)), [nameError]);
assert.deepStrictEqual(toArray(div2.querySelectorAll('span')).map((el) => getVisibleText(el, true)), [ageFizbazError]);
addSpy.reset();
removeSpy.reset();
showResultsSpy.reset();
removeResultsSpy.reset();
input2.value = '15';
input2.dispatchEvent(new ctx.Event('change'));
await platform.domQueue.yield();
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
addArgs = addSpy.calls;
removeArgs = removeSpy.calls;
showResultsArgs = showResultsSpy.calls;
removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 1);
assert.equal(addArgs.length, 1);
assert.equal(removeResultsArgs.length, 1);
assert.equal(showResultsArgs.length, 1);
assert.deepStrictEqual([removeArgs[0][0], ...getResult(removeArgs[0][1])], [input2, true, undefined, false, ageFizbazError]);
assert.deepStrictEqual([addArgs[0][0], ...getResult(addArgs[0][1])], [input2, true, undefined, true, undefined]);
assert.deepStrictEqual([(removeResultsArgs[0][0] as HTMLElement).tagName, ...getResult(removeResultsArgs[0][1])], ['DIV', true, undefined, false, ageFizbazError]);
assert.deepStrictEqual([(showResultsArgs[0][0] as HTMLElement).tagName, ...getResult(showResultsArgs[0][1])], ['DIV', true, undefined, true, undefined]);
assert.deepStrictEqual(toArray(div1.querySelectorAll('span')).map((el) => getVisibleText(el, true)), [nameError]);
assert.deepStrictEqual(toArray(div2.querySelectorAll('span')).map((el) => getVisibleText(el, true)), []);
},
{
template: `
<div>
<input id="target1" type="text" value.two-way="person.name & validate">
</div>
<div>
<input id="target2" type="text" value.two-way="person.age|toNumber & validate">
</div>
` }
);
$it('custom presenter implementation can be used to tweak presentation',
async function ({ host, platform, app, ctx }) {
const validationContainer1 = host.querySelector('custom-validation-container');
const validationContainer2 = host.querySelector('custom-validation-container:nth-of-type(2)');
const nameError = 'Name is required.';
const ageRequiredError = 'Age is required.';
const ageFizbazError = 'Age is not fizbaz';
const controller = app.controller;
const presenterService = app.presenterService;
assert.instanceOf(presenterService, CustomPresenterService);
assertSubscriber(controller, presenterService);
assertSubscriber(controller, presenterService);
const input1: HTMLInputElement = validationContainer1.querySelector('input#target1');
const input2: HTMLInputElement = validationContainer2.querySelector('input#target2');
const controllerSpy = app.controllerValidateSpy;
const spy = createSpy(presenterService, 'handleValidationEvent', true);
const addSpy = createSpy(presenterService, 'add', true);
const removeSpy = createSpy(presenterService, 'remove', true);
const showResultsSpy = createSpy(presenterService, 'showResults', true);
const removeResultsSpy = createSpy(presenterService, 'removeResults', true);
await assertEventHandler(input1, platform, controllerSpy, spy, ctx);
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
let addArgs = addSpy.calls;
let removeArgs = removeSpy.calls;
let showResultsArgs = showResultsSpy.calls;
let removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 0);
assert.equal(addArgs.length, 2);
assert.equal(removeResultsArgs.length, 0);
assert.equal(showResultsArgs.length, 2);
assert.deepStrictEqual([addArgs[0][0], ...getResult(addArgs[0][1])], [input1, false, nameError]);
assert.deepStrictEqual([addArgs[1][0], ...getResult(addArgs[1][1])], [input2, false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual([(showResultsArgs[0][0] as HTMLElement).tagName, ...getResult(showResultsArgs[0][1])], ['SMALL', false, nameError]);
assert.deepStrictEqual([(showResultsArgs[1][0] as HTMLElement).tagName, ...getResult(showResultsArgs[1][1])], ['SMALL', false, ageRequiredError, false, ageFizbazError]);
assert.equal(getVisibleText(validationContainer1.shadowRoot.querySelector('small'), true), nameError);
assert.equal(getVisibleText(validationContainer2.shadowRoot.querySelector('small'), true), `${ageRequiredError}${ageFizbazError}`);
addSpy.reset();
removeSpy.reset();
showResultsSpy.reset();
removeResultsSpy.reset();
input2.value = '22';
input2.dispatchEvent(new ctx.Event('change'));
await platform.domQueue.yield();
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
addArgs = addSpy.calls;
removeArgs = removeSpy.calls;
showResultsArgs = showResultsSpy.calls;
removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 1);
assert.equal(addArgs.length, 1);
assert.equal(removeResultsArgs.length, 1);
assert.equal(showResultsArgs.length, 1);
assert.deepStrictEqual([removeArgs[0][0], ...getResult(removeArgs[0][1])], [input2, false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual([addArgs[0][0], ...getResult(addArgs[0][1])], [input2, true, undefined, false, ageFizbazError]);
assert.deepStrictEqual([(removeResultsArgs[0][0] as HTMLElement).tagName, ...getResult(removeResultsArgs[0][1])], ['SMALL', false, ageRequiredError, false, ageFizbazError]);
assert.deepStrictEqual([(showResultsArgs[0][0] as HTMLElement).tagName, ...getResult(showResultsArgs[0][1])], ['SMALL', true, undefined, false, ageFizbazError]);
assert.equal(getVisibleText(validationContainer1.shadowRoot.querySelector('small'), true), nameError);
assert.equal(getVisibleText(validationContainer2.shadowRoot.querySelector('small'), true), ageFizbazError);
addSpy.reset();
removeSpy.reset();
showResultsSpy.reset();
removeResultsSpy.reset();
input2.value = '15';
input2.dispatchEvent(new ctx.Event('change'));
await platform.domQueue.yield();
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
addArgs = addSpy.calls;
removeArgs = removeSpy.calls;
showResultsArgs = showResultsSpy.calls;
removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 1);
assert.equal(addArgs.length, 1);
assert.equal(removeResultsArgs.length, 1);
assert.equal(showResultsArgs.length, 1);
assert.deepStrictEqual([removeArgs[0][0], ...getResult(removeArgs[0][1])], [input2, true, undefined, false, ageFizbazError]);
assert.deepStrictEqual([addArgs[0][0], ...getResult(addArgs[0][1])], [input2, true, undefined, true, undefined]);
assert.deepStrictEqual([(removeResultsArgs[0][0] as HTMLElement).tagName, ...getResult(removeResultsArgs[0][1])], ['SMALL', true, undefined, false, ageFizbazError]);
assert.deepStrictEqual([(showResultsArgs[0][0] as HTMLElement).tagName, ...getResult(showResultsArgs[0][1])], ['SMALL', true, undefined, true, undefined]);
assert.equal(getVisibleText(validationContainer1.shadowRoot.querySelector('small'), true), nameError);
assert.equal(getVisibleText(validationContainer2.shadowRoot.querySelector('small'), true), '');
},
{
presenterServiceFactory(x) { return x.invoke(CustomPresenterService); },
template: `
<custom-validation-container>
<input id="target1" type="text" value.two-way="person.name & validate">
</custom-validation-container>
<custom-validation-container>
<input id="target2" type="text" value.two-way="person.age|toNumber & validate">
</custom-validation-container>
` }
);
$it('does not add/remove results if the container returned is null',
async function ({ host, platform, app, ctx }) {
const validationContainer1 = host.querySelector('div');
const validationContainer2 = host.querySelector('div:nth-of-type(2)');
const controller = app.controller;
const presenterService = app.presenterService;
assertSubscriber(controller, presenterService);
assertSubscriber(controller, presenterService);
const input1: HTMLInputElement = validationContainer1.querySelector('input#target1');
const input2: HTMLInputElement = validationContainer2.querySelector('input#target2');
const controllerSpy = app.controllerValidateSpy;
const spy = createSpy(presenterService, 'handleValidationEvent', true);
const addSpy = createSpy(presenterService, 'add', true);
const removeSpy = createSpy(presenterService, 'remove', true);
const showResultsSpy = createSpy(presenterService, 'showResults', true);
const removeResultsSpy = createSpy(presenterService, 'removeResults', true);
await assertEventHandler(input1, platform, controllerSpy, spy, ctx);
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
let addArgs = addSpy.calls;
let removeArgs = removeSpy.calls;
let showResultsArgs = showResultsSpy.calls;
let removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 0);
assert.equal(addArgs.length, 2);
assert.equal(removeResultsArgs.length, 0);
assert.equal(showResultsArgs.length, 0);
addSpy.reset();
removeSpy.reset();
showResultsSpy.reset();
removeResultsSpy.reset();
input2.value = '22';
input2.dispatchEvent(new ctx.Event('change'));
await platform.domQueue.yield();
await assertEventHandler(input2, platform, controllerSpy, spy, ctx);
addArgs = addSpy.calls;
removeArgs = removeSpy.calls;
showResultsArgs = showResultsSpy.calls;
removeResultsArgs = removeResultsSpy.calls;
assert.equal(removeArgs.length, 1);
assert.equal(addArgs.length, 1);
assert.equal(removeResultsArgs.length, 0);
assert.equal(showResultsArgs.length, 0);
},
{
presenterServiceFactory(x) {
return x.invoke(class extends ValidationResultPresenterService {
public getValidationMessageContainer() { return null; }
});
},
template: `
<div>
<input id="target1" type="text" value.two-way="person.name & validate">
</div>
<div>
<input id="target2" type="text" value.two-way="person.age|toNumber & validate">
</div>
` }
);
});
});