Files
react-native/packages/react-native-compatibility-check/src/__tests__/VersionDiffing-test.js
T
Eli White 60e3921f9c Initial Open Sourcing of React Native Compatibility Check (#49340)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49340

This tool enables checking the boundary between JavaScript and Native for
backwards incompatible changes to protect against crashes.

This is useful for:

- Local Development
- Over the Air updates on platforms that support it
- Theoretically: Server Components with React Native

Check out the Readme for more information

Changelog: [General][Added] Open Sourcing React Native's Compatibility Check

Reviewed By: panagosg7

Differential Revision: D69476742

fbshipit-source-id: 8af6039839c5475c1258fa82d9750a9320cf0751
2025-02-12 08:24:13 -08:00

1604 lines
52 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
* @oncall react_native
*/
import type {SchemaDiff, TypeStore} from '../DiffResults';
import {schemaDiffExporter} from '../DiffResults.js';
import {
addedEnumMessage,
addedPropertiesMessage,
addedUnionMessage,
buildSchemaDiff,
hasUpdatesTypes,
removedEnumMessage,
removedPropertiesMessage,
removedUnionMessage,
summarizeDiffSet,
tooOptionalPropertiesMessage,
typeNullableChangeMessage,
} from '../VersionDiffing.js';
import {getTestSchema} from './utilities/getTestSchema.js';
const schemaBeforeAfterTypes = makeRN(
'native-module-before-after-types/NativeModuleBeforeAfterTypes',
);
const schemaBeforeAfterTypesTypesRemoved = makeRN(
'native-module-before-after-types-removed/NativeModuleBeforeAfterTypes',
);
const schemaWithRN = makeRN('native-module/NativeModule');
const schemaWithRNChanged = makeRN('native-module-changed/NativeModule');
const schemaGetConstants = makeRN('native-module-get-constants/NativeModule');
const schemaGetConstantsAddedRequiredConstant = makeRN(
'native-module-get-constants-added-required-constant/NativeModule',
);
const schemaGetConstantsAddedOptionalConstant = makeRN(
'native-module-get-constants-added-optional-constant/NativeModule',
);
const schemaGetConstantsReadOnly = makeRN(
'native-module-get-constants-readonly/NativeModule',
);
const schemaGetConstantsAddedRequiredConstantReadOnly = makeRN(
'native-module-get-constants-added-required-constant-readonly/NativeModule',
);
const schemaGetConstantsAddedOptionalConstantReadOnly = makeRN(
'native-module-get-constants-added-optional-constant-readonly/NativeModule',
);
const schemaBeforeAfterTypesTypeChanged = makeRN(
'native-module-before-after-types-type-changed/NativeModuleBeforeAfterTypes',
);
function makeRN(name: string) {
return getTestSchema(__dirname, '__fixtures__', name + '.js.flow');
}
const schemaWithRnNestedObject = makeRN('native-module-nested/NativeModule');
const schemaWithRnNestedChanged = makeRN(
'native-module-nested-changed/NativeModule',
);
const schemaWithRnNestedOptions = makeRN(
'native-module-nested-optional/NativeModule',
);
const schemaWithRnNestedNullable = makeRN(
'native-module-nested-nullable/NativeModule',
);
const schemaWithEnum = makeRN('native-module-with-enum/NativeModule');
const schemaWithEnumChanges = makeRN(
'native-module-with-enum-changes/NativeModule',
);
const schemaWithEnumTypeChanges = makeRN(
'native-module-with-enum-type-changes/NativeModule',
);
const schemaWithEnumValueChanges = makeRN(
'native-module-with-enum-value-changes/NativeModule',
);
const schemaWithEnumFromNative = makeRN(
'native-module-with-enum-from-native/NativeModule',
);
const schemaWithEnumFromNativeChanges = makeRN(
'native-module-with-enum-from-native-changes/NativeModule',
);
const schemaWithEnumFromNativeTypeChanges = makeRN(
'native-module-with-enum-from-native-type-changes/NativeModule',
);
const schemaWithEnumFromNativeValueChanges = makeRN(
'native-module-with-enum-from-native-value-changes/NativeModule',
);
const schemaWithEventEmitter = makeRN(
'native-module-with-eventemitter/NativeModule',
);
const schemaWithEventEmitterChanges = makeRN(
'native-module-with-eventemitter-changes/NativeModule',
);
const schemaWithOptionalArgument = makeRN(
'native-module-with-optional-argument/NativeModule',
);
const schemaWithUnion = makeRN('native-module-with-union/NativeModule');
const schemaWithUnionChanges = makeRN(
'native-module-with-union-changes/NativeModule',
);
const schemaWithUnionFromNative = makeRN(
'native-module-with-union-from-native/NativeModule',
);
const schemaWithUnionFromNativeChanges = makeRN(
'native-module-with-union-from-native-changes/NativeModule',
);
const schemaWithRNNativeComponent = makeRN('native-component/NativeComponent');
const schemaNativeComponentWithCommand = makeRN(
'native-component-with-command/NativeComponent',
);
const schemaNativeComponentWithCommandExtraArg = makeRN(
'native-component-with-command-extra-arg/NativeComponent',
);
const schemaNativeComponentWithCommandExtraCommand = makeRN(
'native-component-with-command-extra-command/NativeComponent',
);
const schemaNativeComponentWithProps = makeRN(
'native-component-with-props/NativeComponent',
);
const schemaNativeComponentWithPropsAddedOptionalProp = makeRN(
'native-component-with-props-added-optional-prop/NativeComponent',
);
const schemaNativeComponentWithPropsAddedRequiredProp = makeRN(
'native-component-with-props-added-required-prop/NativeComponent',
);
const schemaNativeComponentWithPropsChanges = makeRN(
'native-component-with-props-changes/NativeComponent',
);
const schemaNativeComponentWithPropsDefaultChange = makeRN(
'native-component-with-props-default-change/NativeComponent',
);
const schemaNativeComponentWithPropsNestedObject = makeRN(
'native-component-with-props-nested-object/NativeComponent',
);
const schemaNativeComponentWithPropsNestedObjectAddedOptionalKey = makeRN(
'native-component-with-props-nested-object-added-optional-key/NativeComponent',
);
const schemaNativeComponentWithPropsNestedObjectAddedRequiredKey = makeRN(
'native-component-with-props-nested-object-added-required-key/NativeComponent',
);
const schemaNativeComponentWithPropsUnion = makeRN(
'native-component-with-props-union/NativeComponent',
);
const schemaNativeComponentWithPropsUnionAdded = makeRN(
'native-component-with-props-union-added/NativeComponent',
);
const schemaNativeComponentWithPropsArrayUnion = makeRN(
'native-component-with-props-array-union/NativeComponent',
);
const schemaNativeComponentWithPropsArrayUnionAdded = makeRN(
'native-component-with-props-array-union-added/NativeComponent',
);
describe('hasUpdates', () => {
it('is given empty sets', () => {
expect(
hasUpdatesTypes({
newTypes: new Set(),
deprecatedTypes: new Set(),
objectTypeChanges: new Set(),
incompatibleChanges: new Set(),
}),
).toBeFalsy();
});
const set = new Set<TypeStore>();
set.add({
typeName: 'name',
typeInformation: {
type: 'AnyTypeAnnotation',
},
filename: 'name',
});
it('is given nonempty newTypes', () => {
expect(
hasUpdatesTypes({
newTypes: set,
deprecatedTypes: new Set(),
objectTypeChanges: new Set(),
incompatibleChanges: new Set(),
}),
).toBeTruthy();
});
it('is given nonempty deprecatedTypes', () => {
expect(
hasUpdatesTypes({
newTypes: new Set(),
deprecatedTypes: set,
objectTypeChanges: new Set(),
incompatibleChanges: new Set(),
}),
).toBeTruthy();
});
});
describe('buildSchemaDiff', () => {
it('same file, no changes', () => {
expect(
buildSchemaDiff(schemaBeforeAfterTypes, schemaBeforeAfterTypes),
).toEqual(new Set());
});
it('same RN file, no changes', () => {
expect(buildSchemaDiff(schemaWithRN, schemaWithRN)).toEqual(new Set());
});
it('two RN files, with incompatible Spec changes', () => {
expect(
schemaDiffExporter(
Array.from(buildSchemaDiff(schemaWithRN, schemaWithRNChanged))[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.anything(),
]),
}),
}),
]),
}),
}),
);
});
it('RN file with nested object properties, no changes', () => {
expect(
buildSchemaDiff(schemaWithRnNestedObject, schemaWithRnNestedObject),
).toEqual(new Set());
});
it('RN file with nested object properties, added', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithRnNestedChanged, schemaWithRnNestedObject),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
objectTypeChanges: expect.any(Object),
incompatibleChanges: expect.not.arrayContaining([
expect.anything(),
]),
}),
}),
]),
}),
}),
);
});
it('RN file with nested object properties, removed', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithRnNestedObject, schemaWithRnNestedChanged),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: removedPropertiesMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
it('RN file with nested object properties, optional removed', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithRnNestedOptions, schemaWithRnNestedObject),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: tooOptionalPropertiesMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
it('RN file with nested object properties, nullable added', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithRnNestedNullable,
schemaWithRnNestedOptions,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: typeNullableChangeMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
it('RN file with nested object properties on function, made optional', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithRnNestedOptions, schemaWithRnNestedChanged),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: tooOptionalPropertiesMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
describe('enums', () => {
describe('toNative', () => {
it('RN with enum types, same module', () => {
expect(buildSchemaDiff(schemaWithEnum, schemaWithEnum)).toEqual(
new Set(),
);
});
it('RN with enum types, and removal', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithEnum, schemaWithEnumChanges),
)[0],
),
).toEqual(
expect.objectContaining({
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
newTypes: expect.not.arrayContaining([expect.anything()]),
deprecatedTypes: expect.not.arrayContaining([
expect.anything(),
]),
incompatibleChanges: expect.not.arrayContaining([
expect.anything(),
]),
objectTypeChanges: expect.any(Object),
}),
}),
]),
}),
}),
);
});
it('RN with enum types, and addition', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithEnumChanges, schemaWithEnum),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorCode: 'addedEnumCases',
errorInformation: expect.objectContaining({
message: addedEnumMessage,
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with enum types, and type changes', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithEnum, schemaWithEnumTypeChanges),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError: expect.objectContaining({
previousError: expect.objectContaining({
message:
'EnumDeclaration member types are not the same',
}),
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with enum types, and value changes', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithEnum, schemaWithEnumValueChanges),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError: expect.objectContaining({
previousError: expect.objectContaining({
message: 'Enum types do not match',
previousError: expect.objectContaining({
message:
'Enum contained a member with a type mismatch',
mismatchedMembers: expect.arrayContaining(
[
expect.objectContaining({
member: 'B',
}),
],
),
}),
}),
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
});
describe('fromNative', () => {
it('RN with enum types, same module', () => {
expect(
buildSchemaDiff(schemaWithEnumFromNative, schemaWithEnumFromNative),
).toEqual(new Set());
});
it('RN with enum types, and removal', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithEnumFromNative,
schemaWithEnumFromNativeChanges,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorCode: 'removedEnumCases',
errorInformation: expect.objectContaining({
message: removedEnumMessage,
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with enum types, and addition', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithEnumFromNativeChanges,
schemaWithEnumFromNative,
),
)[0],
),
).toEqual(
expect.objectContaining({
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
newTypes: expect.not.arrayContaining([expect.anything()]),
deprecatedTypes: expect.not.arrayContaining([
expect.anything(),
]),
incompatibleChanges: expect.not.arrayContaining([
expect.anything(),
]),
objectTypeChanges: expect.any(Object),
}),
}),
]),
}),
}),
);
});
it('RN with enum types, and type changes', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithEnumFromNative,
schemaWithEnumFromNativeTypeChanges,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError: expect.objectContaining({
previousError: expect.objectContaining({
mismatchedProperties:
expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError:
expect.objectContaining({
message:
'EnumDeclaration member types are not the same',
}),
}),
}),
]),
}),
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with enum types, and value changes', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithEnumFromNative,
schemaWithEnumFromNativeValueChanges,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError: expect.objectContaining({
previousError: expect.objectContaining({
mismatchedProperties:
expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError:
expect.objectContaining({
message:
'Enum types do not match',
previousError:
expect.objectContaining({
message:
'Enum contained a member with a type mismatch',
mismatchedMembers:
expect.arrayContaining([
expect.objectContaining(
{
member: 'B',
},
),
]),
}),
}),
}),
}),
]),
}),
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
});
});
it('RN with eventEmitters, same module', () => {
expect(
buildSchemaDiff(schemaWithEventEmitter, schemaWithEventEmitter),
).toEqual(new Set());
});
it('RN with eventEmitters, and eventType changes', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithEventEmitter,
schemaWithEventEmitterChanges,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message:
'Object contained properties with type mismatches',
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
message: 'has conflicting type changes',
previousError: expect.objectContaining({
message:
'EventEmitter eventTypes are not equivalent',
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with adding an optional argument to function', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithOptionalArgument, schemaWithRN),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message:
'Object contained a property with a type mismatch',
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
message: 'has conflicting type changes',
previousError: expect.objectContaining({
message:
'Function types have differing length of arguments',
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with removing an optional argument to function', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithRN, schemaWithOptionalArgument),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message:
'Object contained a property with a type mismatch',
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
message: 'has conflicting type changes',
previousError: expect.objectContaining({
message:
'Function types have differing length of arguments',
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with union types, same module', () => {
expect(buildSchemaDiff(schemaWithUnion, schemaWithUnion)).toEqual(
new Set(),
);
});
it('RN with union types, and addition', () => {
expect(
schemaDiffExporter(
Array.from(buildSchemaDiff(schemaWithUnionChanges, schemaWithUnion))[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message: addedUnionMessage,
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('RN with union types, and removal', () => {
expect(
schemaDiffExporter(
Array.from(buildSchemaDiff(schemaWithUnion, schemaWithUnionChanges))[0],
),
).toEqual(
expect.objectContaining({
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
newTypes: expect.not.arrayContaining([expect.anything()]),
deprecatedTypes: expect.not.arrayContaining([
expect.anything(),
]),
incompatibleChanges: expect.not.arrayContaining([
expect.anything(),
]),
objectTypeChanges: expect.any(Object),
}),
}),
]),
}),
}),
);
});
it('RN with union types from native, and addition', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithUnionFromNativeChanges,
schemaWithUnionFromNative,
),
)[0],
),
).toEqual(
expect.objectContaining({
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
newTypes: expect.not.arrayContaining([expect.anything()]),
deprecatedTypes: expect.not.arrayContaining([
expect.anything(),
]),
incompatibleChanges: expect.not.arrayContaining([
expect.anything(),
]),
objectTypeChanges: expect.any(Object),
}),
}),
]),
}),
}),
);
});
it('RN with union types from native, and removal', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaWithUnionFromNative,
schemaWithUnionFromNativeChanges,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message: removedUnionMessage,
}),
}),
]),
}),
}),
]),
}),
}),
);
});
describe('NativeComponent', () => {
it('module to component; deprecated + new', () => {
expect(
Array.from(buildSchemaDiff(schemaWithRNNativeComponent, schemaWithRN)),
).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'NativeComponent',
status: 'new',
}),
expect.objectContaining({
name: 'NativeModule',
status: 'deprecated',
}),
]),
);
});
it('component changed to module; deprecated + new', () => {
expect(
Array.from(buildSchemaDiff(schemaWithRN, schemaWithRNNativeComponent)),
).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'NativeModule',
status: 'new',
}),
expect.objectContaining({
name: 'NativeComponent',
status: 'deprecated',
}),
]),
);
});
it('same RN component, no changes', () => {
expect(
buildSchemaDiff(
schemaWithRNNativeComponent,
schemaWithRNNativeComponent,
),
).toEqual(new Set());
});
describe('commands', () => {
it('Command with additional arg', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaNativeComponentWithCommandExtraArg,
schemaNativeComponentWithCommand,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError: expect.objectContaining({
message:
'Function types have differing length of arguments',
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('Command with arg removed', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaNativeComponentWithCommand,
schemaNativeComponentWithCommandExtraArg,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
mismatchedProperties: expect.arrayContaining([
expect.objectContaining({
fault: expect.objectContaining({
previousError: expect.objectContaining({
message:
'Function types have differing length of arguments',
}),
}),
}),
]),
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('1 command to 2 commands is allowed', () => {
// This could technically result in an OTA issue if the command
// is called immediately and the native side throws on unrecognized
// commands. However, there is no way to do feature detection
// of commands today to protect against that.
// We are choosing to allow this change to be made since we previously
// had no protection for commands at all.
// See https://fb.workplace.com/groups/615693552291894/posts/1905124013348835
// for more information.
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithCommandExtraCommand,
schemaNativeComponentWithCommand,
),
).status,
).toEqual('ok');
});
it('2 commands to 1 command is allowed', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithCommand,
schemaNativeComponentWithCommandExtraCommand,
),
).status,
).toEqual('ok');
});
it('0 commands to 1 command is allowed', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithCommand,
schemaWithRNNativeComponent,
),
).status,
).toEqual('ok');
});
it('1 command to 0 commands is allowed', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaWithRNNativeComponent,
schemaNativeComponentWithCommand,
),
).status,
).toEqual('ok');
});
});
describe('props', () => {
it('prop with changed value', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaNativeComponentWithPropsChanges,
schemaNativeComponentWithProps,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message:
'Object contained a property with a type mismatch',
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('prop with changed default value should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsDefaultChange,
schemaNativeComponentWithProps,
),
).status,
).toBe('ok');
});
it('props with optional prop added should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsAddedOptionalProp,
schemaNativeComponentWithProps,
),
).status,
).toBe('ok');
});
it('props with optional prop removed should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithProps,
schemaNativeComponentWithPropsAddedOptionalProp,
),
).status,
).toBe('ok');
});
it('props with required prop added should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsAddedRequiredProp,
schemaNativeComponentWithProps,
),
).status,
).toBe('ok');
});
it('props with required prop removed should fail', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaNativeComponentWithProps,
schemaNativeComponentWithPropsAddedRequiredProp,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: removedPropertiesMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
it('props with nested object, adding an optional key should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsNestedObjectAddedOptionalKey,
schemaNativeComponentWithPropsNestedObject,
),
).status,
).toBe('ok');
});
it('props with nested object, removing an optional key should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsNestedObject,
schemaNativeComponentWithPropsNestedObjectAddedOptionalKey,
),
).status,
).toBe('ok');
});
it('props with nested object, adding a required key should be okay', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsNestedObjectAddedRequiredKey,
schemaNativeComponentWithPropsNestedObject,
),
).status,
).toBe('ok');
});
it('props with union and added option should fail', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaNativeComponentWithPropsUnionAdded,
schemaNativeComponentWithPropsUnion,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: addedUnionMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
it('props with union and removed option should be ok', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsUnion,
schemaNativeComponentWithPropsUnionAdded,
),
).status,
).toBe('ok');
});
it('props with array union and added option should fail', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(
schemaNativeComponentWithPropsArrayUnionAdded,
schemaNativeComponentWithPropsArrayUnion,
),
)[0],
),
).toEqual(
expect.objectContaining({
framework: 'ReactNative',
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.objectContaining({
'0': expect.objectContaining({
errorInformation: expect.objectContaining({
message: addedUnionMessage,
}),
}),
}),
}),
}),
]),
}),
}),
);
});
it('props with array union and removed option should be ok', () => {
// This is risky due to these being stored from component props codegen
// in C++ ints via bitshift which might change the resulting
// int value for the same inputs.
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaNativeComponentWithPropsArrayUnion,
schemaNativeComponentWithPropsArrayUnionAdded,
),
).status,
).toBe('ok');
});
});
});
describe('RN new codegen', () => {
describe('Native Module', () => {
it('removed', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(schemaWithRNNativeComponent, schemaWithRN),
).status,
).toEqual('patchable');
});
it('added', () => {
expect(
schemaDiffExporter(
Array.from(
buildSchemaDiff(schemaWithRN, schemaWithRNNativeComponent),
)[0],
),
).toEqual(
expect.objectContaining({
name: 'NativeModule',
status: 'new',
}),
);
});
});
});
});
describe('RN NativeModule getConstants type diffing', () => {
it('should not allow required constants to be added to NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedRequiredConstant, // new
schemaGetConstants, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible');
expect(schemaDiffExporter(Array.from(schemaDiff)[0])).toEqual(
expect.objectContaining({
status: expect.objectContaining({
incompatibleSpecs: expect.arrayContaining([
expect.objectContaining({
changeInformation: expect.objectContaining({
incompatibleChanges: expect.arrayContaining([
expect.objectContaining({
errorInformation: expect.objectContaining({
message: addedPropertiesMessage,
}),
}),
]),
}),
}),
]),
}),
}),
);
});
it('should allow optional constants to be added to NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedOptionalConstant, // new
schemaGetConstants, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
it('should allow removal of required constants from NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstants, // new
schemaGetConstantsAddedRequiredConstant, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
it('should allow removal of optional constants from NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstants, // new
schemaGetConstantsAddedOptionalConstant, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
it('should not allow promotion of optional constants to required constants', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedRequiredConstant, // new
schemaGetConstantsAddedOptionalConstant, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible');
});
it('should allow required constants to become optional constants', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedOptionalConstant, // new
schemaGetConstantsAddedRequiredConstant, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
});
describe('RN NativeModule getConstants $ReadOnly type diffing', () => {
it('should not allow required constants to be added to NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedRequiredConstantReadOnly, // new
schemaGetConstantsReadOnly, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible');
});
it('should allow optional constants to be added to NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedOptionalConstantReadOnly, // new
schemaGetConstantsReadOnly, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
it('should allow removal of required constants from NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsReadOnly, // new
schemaGetConstantsAddedRequiredConstantReadOnly, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
it('should allow removal of optional constants from NativeModules', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsReadOnly, // new
schemaGetConstantsAddedOptionalConstantReadOnly, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
it('should not allow promotion of optional constants to required constants', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedRequiredConstantReadOnly, // new
schemaGetConstantsAddedOptionalConstantReadOnly, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('incompatible');
});
it('should allow required constants to become optional constants', () => {
const schemaDiff = buildSchemaDiff(
schemaGetConstantsAddedOptionalConstantReadOnly, // new
schemaGetConstantsAddedRequiredConstantReadOnly, // old
);
expect(summarizeDiffSet(schemaDiff).status).toBe('ok');
});
});
describe('summarizeDiffSet', () => {
const set = new Set<SchemaDiff>();
it('is given empty set', () => {
expect(summarizeDiffSet(set).status).toBe('ok');
});
it('is given set with one new', () => {
set.add({
name: 'SampleName',
framework: 'ReactNative',
status: 'new',
});
expect(summarizeDiffSet(set).status).toBe('patchable');
});
it('is given complex set', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaBeforeAfterTypes,
schemaBeforeAfterTypesTypesRemoved,
),
).status,
).toBe('incompatible');
});
it('is given a diff that has incompatible changes in the boundary', () => {
expect(
summarizeDiffSet(
buildSchemaDiff(
schemaBeforeAfterTypes,
schemaBeforeAfterTypesTypeChanged,
),
).status,
).toBe('incompatible');
});
});