mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-08-01 01:47:43 -04:00
534 lines
16 KiB
JavaScript
534 lines
16 KiB
JavaScript
import validateProjectFieldsForObservation, {
|
|
INVALID_NUMERIC,
|
|
MISSING_REQUIRED,
|
|
validateProjectFieldValue,
|
|
} from "sharedHelpers/validateProjectFieldsForObservation";
|
|
import factory from "tests/factory";
|
|
|
|
describe( "validateProjectFieldValue", () => {
|
|
describe( "required fields", () => {
|
|
test.each( [
|
|
[undefined],
|
|
[null],
|
|
[""],
|
|
[" "],
|
|
] )( "should return MISSING_REQUIRED for empty value %p", value => {
|
|
const mockPOF = factory( "LocalProjectObservationField", {
|
|
required: true,
|
|
obsField: factory( "LocalObservationField", {
|
|
allowedValues: [],
|
|
} ),
|
|
} );
|
|
expect( validateProjectFieldValue( mockPOF, value ) ).toBe( MISSING_REQUIRED );
|
|
} );
|
|
|
|
it( "should return null for a non-empty value", () => {
|
|
const mockPOF = factory( "LocalProjectObservationField", {
|
|
required: true,
|
|
obsField: factory( "LocalObservationField", {
|
|
allowedValues: [],
|
|
} ),
|
|
} );
|
|
expect( validateProjectFieldValue( mockPOF, "x" ) ).toBeNull( );
|
|
} );
|
|
|
|
it( "should return null for a value that is non-empty after trim", () => {
|
|
const mockPOF = factory( "LocalProjectObservationField", {
|
|
required: true,
|
|
obsField: factory( "LocalObservationField", {
|
|
allowedValues: [],
|
|
} ),
|
|
} );
|
|
expect( validateProjectFieldValue( mockPOF, " x " ) ).toBeNull( );
|
|
} );
|
|
} );
|
|
|
|
describe( "numeric fields", () => {
|
|
it( "should return INVALID_NUMERIC for a non-numeric value on an optional field", () => {
|
|
const mockPOF = factory( "LocalProjectObservationField", {
|
|
required: false,
|
|
obsField: factory( "LocalObservationField", {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
} ),
|
|
} );
|
|
expect( validateProjectFieldValue( mockPOF, "abc" ) ).toBe( INVALID_NUMERIC );
|
|
} );
|
|
|
|
test.each( [
|
|
[undefined],
|
|
[""],
|
|
] )( "should return null for empty value %p on an optional numeric field", value => {
|
|
const mockPOF = factory( "LocalProjectObservationField", {
|
|
required: false,
|
|
obsField: factory( "LocalObservationField", {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
} ),
|
|
} );
|
|
expect( validateProjectFieldValue( mockPOF, value ) ).toBeNull( );
|
|
} );
|
|
} );
|
|
|
|
describe( "optional text fields", () => {
|
|
test.each( [
|
|
[undefined],
|
|
[""],
|
|
["anything"],
|
|
] )( "should return null for %p", value => {
|
|
const mockPOF = factory( "LocalProjectObservationField", {
|
|
required: false,
|
|
obsField: factory( "LocalObservationField", {
|
|
allowedValues: [],
|
|
} ),
|
|
} );
|
|
expect( validateProjectFieldValue( mockPOF, value ) ).toBeNull( );
|
|
} );
|
|
} );
|
|
} );
|
|
|
|
describe( "validateProjectFieldsForObservation", () => {
|
|
describe( "required fields", () => {
|
|
it( "should be valid when a required field has a non-empty OFV", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value: "shrubland" }],
|
|
};
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( true );
|
|
expect( result.errors ).toEqual( [] );
|
|
} );
|
|
|
|
it( "should return MISSING_REQUIRED when a required field has no OFV", () => {
|
|
const mockProject = {
|
|
title: "Mushrooms of Bavaria",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors ).toEqual( [{
|
|
projectId: mockProject.id,
|
|
projectTitle: "Mushrooms of Bavaria",
|
|
obsFieldId: 10,
|
|
fieldName: "Habitat",
|
|
reason: MISSING_REQUIRED,
|
|
}] );
|
|
} );
|
|
|
|
test.each( [
|
|
[""],
|
|
[" "],
|
|
] )( "should return MISSING_REQUIRED when a required field's OFV value is %p", value => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value }],
|
|
};
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors[0].reason ).toBe( MISSING_REQUIRED );
|
|
} );
|
|
|
|
it( "should be valid when a required field's OFV is non-empty after trim", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{
|
|
obsFieldId: 10,
|
|
value: "something",
|
|
}],
|
|
};
|
|
expect(
|
|
validateProjectFieldsForObservation( mockObservation, [mockProject] ).valid,
|
|
).toBe( true );
|
|
} );
|
|
|
|
it( "should be valid when an optional field has no OFV", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: false,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
expect(
|
|
validateProjectFieldsForObservation( mockObservation, [mockProject] ).valid,
|
|
).toBe( true );
|
|
} );
|
|
|
|
it( "should report only the unfilled field when one of two required fields is filled", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [
|
|
{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
},
|
|
{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 20,
|
|
name: "Substrate",
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value: "shrubland" }],
|
|
};
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors ).toHaveLength( 1 );
|
|
expect( result.errors[0].fieldName ).toBe( "Substrate" );
|
|
expect( result.errors[0].reason ).toBe( MISSING_REQUIRED );
|
|
} );
|
|
|
|
it( "should report both fields in POF order when two required fields are unfilled", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [
|
|
{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
},
|
|
{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 20,
|
|
name: "Substrate",
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors.map( e => e.fieldName ) ).toEqual( ["Habitat", "Substrate"] );
|
|
} );
|
|
} );
|
|
|
|
describe( "numeric fields", () => {
|
|
test.each( [
|
|
["12.5"],
|
|
["42"],
|
|
["-3.2"],
|
|
[" 7 "],
|
|
] )( "should be valid when a required numeric field's OFV value is %p", value => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value }],
|
|
};
|
|
expect(
|
|
validateProjectFieldsForObservation( mockObservation, [mockProject] ).valid,
|
|
).toBe( true );
|
|
} );
|
|
|
|
test.each( [
|
|
["abc"],
|
|
// parseFloat would accept this, Number does not; must stay invalid
|
|
// to match Android Legacy's Float.valueOf
|
|
["1.5abc"],
|
|
] )( "should return INVALID_NUMERIC when a numeric field's OFV value is %p", value => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
id: 10,
|
|
name: "Count",
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value }],
|
|
};
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors ).toHaveLength( 1 );
|
|
expect( result.errors[0].fieldName ).toBe( "Count" );
|
|
expect( result.errors[0].reason ).toBe( INVALID_NUMERIC );
|
|
} );
|
|
|
|
it( "should return INVALID_NUMERIC for an optional numeric field with text value", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: false,
|
|
obsField: {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value: "abc" }],
|
|
};
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors[0].reason ).toBe( INVALID_NUMERIC );
|
|
} );
|
|
|
|
test.each( [
|
|
[[]],
|
|
[[{ obsFieldId: 10, value: "" }]],
|
|
] )( "should be valid for an optional numeric field with OFVs %p", observationFieldValues => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: false,
|
|
obsField: {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues };
|
|
expect(
|
|
validateProjectFieldsForObservation( mockObservation, [mockProject] ).valid,
|
|
).toBe( true );
|
|
} );
|
|
|
|
it( "should report a single MISSING_REQUIRED when a required numeric field is empty", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
datatype: "numeric",
|
|
id: 10,
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
const result = validateProjectFieldsForObservation( mockObservation, [mockProject] );
|
|
expect( result.errors ).toHaveLength( 1 );
|
|
expect( result.errors[0].reason ).toBe( MISSING_REQUIRED );
|
|
} );
|
|
} );
|
|
|
|
describe( "multiple projects", () => {
|
|
it( "should be valid when one OFV satisfies two projects requiring the same field", () => {
|
|
const mockProjectA = {
|
|
id: 1,
|
|
title: "Project A",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockProjectB = {
|
|
id: 2,
|
|
title: "Project B",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value: "shrubland" }],
|
|
};
|
|
const result = validateProjectFieldsForObservation(
|
|
mockObservation,
|
|
[mockProjectA, mockProjectB],
|
|
);
|
|
expect( result.valid ).toBe( true );
|
|
expect( result.errors ).toEqual( [] );
|
|
} );
|
|
|
|
it( "should report one error per project when a shared required field is unfilled", () => {
|
|
const mockProjectA = {
|
|
id: 1,
|
|
title: "Project A",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockProjectB = {
|
|
id: 2,
|
|
title: "Project B",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
const result = validateProjectFieldsForObservation(
|
|
mockObservation,
|
|
[mockProjectA, mockProjectB],
|
|
);
|
|
expect( result.valid ).toBe( false );
|
|
expect( result.errors ).toHaveLength( 2 );
|
|
expect( result.errors.map( e => e.projectTitle ) ).toEqual( ["Project A", "Project B"] );
|
|
expect( result.errors.map( e => e.fieldName ) ).toEqual( ["Habitat", "Habitat"] );
|
|
} );
|
|
|
|
it( "should report only the failing project when the other is satisfied", () => {
|
|
const mockProjectA = {
|
|
id: 1,
|
|
title: "Project A",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockProjectB = {
|
|
id: 2,
|
|
title: "Project B",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 20,
|
|
name: "Substrate",
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = {
|
|
observationFieldValues: [{ obsFieldId: 10, value: "shrubland" }],
|
|
};
|
|
const result = validateProjectFieldsForObservation(
|
|
mockObservation,
|
|
[mockProjectA, mockProjectB],
|
|
);
|
|
expect( result.errors ).toHaveLength( 1 );
|
|
expect( result.errors[0].projectTitle ).toBe( "Project B" );
|
|
expect( result.errors[0].fieldName ).toBe( "Substrate" );
|
|
} );
|
|
|
|
it( "should report errors in the order the projects were passed in", () => {
|
|
const mockProjectA = {
|
|
id: 1,
|
|
title: "Project A",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 10,
|
|
name: "Habitat",
|
|
},
|
|
}],
|
|
};
|
|
const mockProjectB = {
|
|
id: 2,
|
|
title: "Project B",
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: {
|
|
allowedValues: [],
|
|
id: 20,
|
|
name: "Substrate",
|
|
},
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
const resultBA = validateProjectFieldsForObservation(
|
|
mockObservation,
|
|
[mockProjectB, mockProjectA],
|
|
);
|
|
expect( resultBA.errors.map( e => e.projectTitle ) ).toEqual( ["Project B", "Project A"] );
|
|
} );
|
|
} );
|
|
|
|
describe( "edge cases", () => {
|
|
it( "should be valid when no projects are passed", () => {
|
|
const mockObservation = { observationFieldValues: [] };
|
|
const result = validateProjectFieldsForObservation( mockObservation, [] );
|
|
expect( result.valid ).toBe( true );
|
|
expect( result.errors ).toEqual( [] );
|
|
} );
|
|
|
|
it( "should be valid when a project has no POFs", () => {
|
|
const mockProject = { projectObservationFields: [] };
|
|
const mockObservation = { observationFieldValues: [] };
|
|
expect(
|
|
validateProjectFieldsForObservation( mockObservation, [mockProject] ).valid,
|
|
).toBe( true );
|
|
} );
|
|
|
|
it( "should skip a POF without an obsField definition", () => {
|
|
const mockProject = {
|
|
projectObservationFields: [{
|
|
required: true,
|
|
obsField: undefined,
|
|
}],
|
|
};
|
|
const mockObservation = { observationFieldValues: [] };
|
|
expect(
|
|
validateProjectFieldsForObservation( mockObservation, [mockProject] ).valid,
|
|
).toBe( true );
|
|
} );
|
|
} );
|
|
} );
|