Skip to content

Commit

Permalink
WIP Fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski committed Nov 9, 2023
1 parent a3fdb5f commit e973b08
Show file tree
Hide file tree
Showing 29 changed files with 955 additions and 961 deletions.
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const planetsField = universeField.at('planets');
// ⮕ Field<Planet[] | undefined>
```

`planetsField` is a derived field, and it is linked to its parent `universeField`.
`planetsField` is a child field, and it is linked to its parent `universeField`.

```ts
planetsField.key;
Expand All @@ -126,18 +126,18 @@ universeField.at('planets');
// ⮕ planetsField
```

So most of the time you don't need to store a derived field in a variable if you already have a reference to a parent
So most of the time you don't need to store a child field in a variable if you already have a reference to a parent
field.

The derived field has all the same functionality as its parent, so you can derive a new field from it as well:
The child field has all the same functionality as its parent, so you can derive a new field from it as well:

```ts
planetsField.at(0).at('name');
// ⮕ Field<string>
```

When a value is set to a derived field, a parent field value is also updated. If parent field doesn't have a value yet,
Roqueform would infer its type from on the derived field key.
When a value is set to a child field, a parent field value is also updated. If parent field doesn't have a value yet,
Roqueform would infer its type from on the child field key.

```ts
universeField.value;
Expand All @@ -152,7 +152,7 @@ universeField.value;
By default, for a string key a parent object is created, and for a number key a parent array is created. You can change
this behaviour with [custom accessors](#accessors).

When a value is set to a parent field, derived fields are also updated:
When a value is set to a parent field, child fields are also updated:

```ts
const nameField = universeField.at('planets').at(0).at('name');
Expand All @@ -171,19 +171,19 @@ nameField.value;
You can subscribe a subscriber to a field updates. The returned callback would unsubscribe the subscriber.

```ts
const unsubscribe = planetsField.subscribe((updatedField, currentField) => {
const unsubscribe = planetsField.on('*', (updatedField, currentField) => {
// Handle the update
});
// ⮕ () => void
```

Listeners are called with two arguments:
Subscribers are called with two arguments:

<dl>
<dt><code>updatedField</code></dt>
<dd>

The field that initiated the update. In this example it can be `planetsField` itself, any of its derived fields, or any
The field that initiated the update. In this example it can be `planetsField` itself, any of its child fields, or any
of its ancestor fields.

</dd>
Expand All @@ -195,15 +195,15 @@ The field to which the subscriber is subscribed. In this example it is `planetsF
</dd>
</dl>

Listeners are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin).
The root field and all derived fields are updated before subscribers are called, so it's safe to read field values in a
Subscribers are called when a field value is changed or [when a plugin mutates the field object](#authoring-a-plugin).
The root field and all child fields are updated before subscribers are called, so it's safe to read field values in a
subscriber.

Fields use [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison to detect that the
value has changed.

```ts
planetsField.at(0).at('name').subscribe(subscriber);
planetsField.at(0).at('name').on('*', subscriber);

// ✅ The subscriber is called
planetsField.at(0).at('name').setValue('Mercury');
Expand All @@ -215,10 +215,10 @@ planetsField.at(0).setValue({ name: 'Mercury' });
# Transient updates

When you call [`setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue) on a field
then subscribers of its ancestors and its updated derived fields are triggered. To manually control the update propagation
then subscribers of its ancestors and its updated child fields are triggered. To manually control the update propagation
to fields ancestors, you can use transient updates.

When a value of a derived field is set transiently, values of its ancestors _aren't_ immediately updated.
When a value of a child field is set transiently, values of its ancestors _aren't_ immediately updated.

```ts
const avatarField = createField();
Expand All @@ -240,7 +240,7 @@ avatarField.at('eyeColor').isTransient;
// ⮕ true
```

To propagate the transient value contained by the derived field to its parent, use
To propagate the transient value contained by the child field to its parent, use
the [`dispatch`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#dispatch) method:

```ts
Expand All @@ -253,7 +253,7 @@ avatarField.value;
`setTransientValue` can be called multiple times, but only the most recent update is propagated to the parent field
after the `dispatch` call.

When a derived field is in a transient state, its value as observed from the parent may differ from the actual value:
When a child field is in a transient state, its value as observed from the parent may differ from the actual value:

```ts
const planetsField = createField(['Mars', 'Pluto']);
Expand Down Expand Up @@ -282,20 +282,20 @@ planetsField.at(1).value;

# Accessors

[`Accessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates
[`ValueAccessor`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html) creates, reads and updates
field values.

- When the new field is derived via
- When the new field is child via
[`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#at) method, the field value is
read from the value of the parent field using the
[`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.
[`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.

- When a field value is updated via
[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Field.html#setValue), then the parent
field value is updated with the value returned from the
[`Accessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the
updated field has derived fields, their values are updated with values returned from the
[`Accessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.
[`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the
updated field has child fields, their values are updated with values returned from the
[`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method.

You can explicitly provide a custom accessor along with the initial value. Be default, Roqueform uses
[`naturalAccessor`](https://smikhalevski.github.io/roqueform/variables/roqueform.naturalAccessor.html):
Expand Down Expand Up @@ -375,7 +375,7 @@ planetField.element;
// ⮕ null
```

The plugin would be called for each derived field when it is first accessed:
The plugin would be called for each child field when it is first accessed:

```ts
planetField.at('name').element
Expand Down Expand Up @@ -413,7 +413,7 @@ Now when `setElement` is called on a field, its subscribers would be invoked.
```ts
const planetField = createField({ name: 'Mars' }, elementPlugin);

planetField.at('name').subscribe((updatedField, currentField) => {
planetField.at('name').on('*', (updatedField, currentField) => {
// Handle the update
currentField.element;
// ⮕ document.body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ConstraintValidationPlugin {
element: Element | null;

/**
* `true` if the field or any of its derived fields have {@link error an associated error}, or `false` otherwise.
* `true` if the field or any of its child fields have {@link error an associated error}, or `false` otherwise.
*/
readonly isInvalid: boolean;

Expand Down Expand Up @@ -64,12 +64,12 @@ export interface ConstraintValidationPlugin {
deleteError(): void;

/**
* Recursively deletes errors associated with this field and all of its derived fields.
* Recursively deletes errors associated with this field and all of its child fields.
*/
clearErrors(): void;

/**
* Shows error message balloon for the first element that is associated with this field or any of its derived fields,
* Shows error message balloon for the first element that is associated with this field or any of its child fields,
* that has an associated error via calling
* {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity reportValidity}.
*
Expand Down Expand Up @@ -108,7 +108,7 @@ export function constraintValidationPlugin(): PluginInjector<ConstraintValidatio
field.errorCount = 0;

Object.defineProperties(field, {
isInvalid: { get: () => field.errorCount !== 0 },
isInvalid: { configurable: true, get: () => field.errorCount !== 0 },
});

const changeListener = (event: Event): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,58 @@ describe('constraintValidationPlugin', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

expect(field.isInvalid).toBe(false);
expect(field.error).toBe(null);
expect(field.error).toBeNull();

expect(field.at('foo').isInvalid).toBe(false);
expect(field.at('foo').error).toBe(null);
expect(field.at('foo').error).toBeNull();
});

test('sets an error to the field that does not have an associated element', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

const subscriberMock = jest.fn();
const fooListenerMock = jest.fn();
const fooSubscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.at('foo').subscribe(fooListenerMock);
field.on('*', subscriberMock);
field.at('foo').on('*', fooSubscriberMock);

field.at('foo').setError('aaa');

expect(field.isInvalid).toBe(true);
expect(field.error).toBe(null);
expect(field.error).toBeNull();
expect(field.at('foo').isInvalid).toBe(true);
expect(field.at('foo').error).toBe('aaa');

expect(subscriberMock).toHaveBeenCalledTimes(1);
expect(fooListenerMock).toHaveBeenCalledTimes(1);
expect(fooSubscriberMock).toHaveBeenCalledTimes(1);
});

test('setting an error to the parent field does not affect the child field', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

const subscriberMock = jest.fn();
const fooListenerMock = jest.fn();
const fooSubscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.at('foo').subscribe(fooListenerMock);
field.on('*', subscriberMock);
field.at('foo').on('*', fooSubscriberMock);

field.setError('aaa');

expect(field.isInvalid).toBe(true);
expect(field.error).toBe('aaa');
expect(field.at('foo').isInvalid).toBe(false);
expect(field.at('foo').error).toBe(null);
expect(field.at('foo').error).toBeNull();

expect(subscriberMock).toHaveBeenCalledTimes(1);
expect(fooListenerMock).toHaveBeenCalledTimes(0);
expect(fooSubscriberMock).toHaveBeenCalledTimes(0);
});

test('does not notify the field if the same error is set', () => {
const field = createField(0, constraintValidationPlugin());

const subscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.on('*', subscriberMock);

field.setError('aaa');
field.setError('aaa');
Expand All @@ -81,33 +81,33 @@ describe('constraintValidationPlugin', () => {

const subscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.on('*', subscriberMock);

field.setError('aaa');
field.deleteError();

expect(field.isInvalid).toBe(false);
expect(field.error).toBe(null);
expect(field.error).toBeNull();
expect(subscriberMock).toHaveBeenCalledTimes(2);
});

test('clears an error of a derived field', () => {
test('clears an error of a child field', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

const subscriberMock = jest.fn();
const fooListenerMock = jest.fn();
const fooSubscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.at('foo').subscribe(fooListenerMock);
field.on('*', subscriberMock);
field.at('foo').on('*', fooSubscriberMock);

field.at('foo').setError('aaa');
field.clearErrors();

expect(field.isInvalid).toBe(false);
expect(field.error).toBe(null);
expect(field.error).toBeNull();

expect(field.at('foo').isInvalid).toBe(false);
expect(field.at('foo').error).toBe(null);
expect(field.at('foo').error).toBeNull();
});

test('reports validity of the root field', () => {
Expand All @@ -127,7 +127,7 @@ describe('constraintValidationPlugin', () => {
expect(field.at('foo').reportValidity()).toBe(true);
});

test('reports validity of the derived field', () => {
test('reports validity of the child field', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

field.at('foo').setError('aaa');
Expand All @@ -149,7 +149,7 @@ describe('constraintValidationPlugin', () => {
field.at('foo').ref(element);

expect(field.isInvalid).toBe(true);
expect(field.error).toBe(null);
expect(field.error).toBeNull();

expect(field.at('foo').isInvalid).toBe(true);
expect(field.at('foo').error).toEqual('Constraints not satisfied');
Expand All @@ -160,7 +160,7 @@ describe('constraintValidationPlugin', () => {

const subscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.on('*', subscriberMock);

element.required = true;

Expand All @@ -173,18 +173,18 @@ describe('constraintValidationPlugin', () => {
field.ref(null);

expect(field.isInvalid).toBe(false);
expect(field.error).toBe(null);
expect(field.error).toBeNull();
expect(subscriberMock).toHaveBeenCalledTimes(2);
});

test('notifies the field when the value is changed', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

const subscriberMock = jest.fn();
const fooListenerMock = jest.fn();
const fooSubscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.at('foo').subscribe(fooListenerMock);
field.on('*', subscriberMock);
field.at('foo').on('*', fooSubscriberMock);

element.value = 'aaa';
element.required = true;
Expand All @@ -200,17 +200,17 @@ describe('constraintValidationPlugin', () => {
fireEvent.change(element, { target: { value: '' } });

expect(subscriberMock).toHaveBeenCalledTimes(1);
expect(fooListenerMock).toHaveBeenCalledTimes(1);
expect(fooSubscriberMock).toHaveBeenCalledTimes(1);
});

test('does not notify an already invalid parent', () => {
const field = createField({ foo: 0 }, constraintValidationPlugin());

const subscriberMock = jest.fn();
const fooListenerMock = jest.fn();
const fooSubscriberMock = jest.fn();

field.subscribe(subscriberMock);
field.at('foo').subscribe(fooListenerMock);
field.on('*', subscriberMock);
field.at('foo').on('*', fooSubscriberMock);

element.value = 'aaa';
element.required = true;
Expand All @@ -226,6 +226,6 @@ describe('constraintValidationPlugin', () => {
fireEvent.change(element, { target: { value: '' } });

expect(subscriberMock).toHaveBeenCalledTimes(1);
expect(fooListenerMock).toHaveBeenCalledTimes(1);
expect(fooSubscriberMock).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit e973b08

Please sign in to comment.