Files
react-native/scripts/diff-api-snapshot/__tests__/diffApiSnapshot-test.js
T
Dawid Małecki 6b40f35032 Add diff-api-snapshot for public API breaking change detection (#51972)
Summary:
This diff adds snapshot `diff-api-snapshot` script for public JS API breaking change detection.

### Motivation
Detecting if there are any breaking changes introduced in the commit. It is achieved by comparing `ReactNativeApi.d.ts` rollup from the current and previous revision.

This is a naive implementation with a three possible outcomes:
- BREAKING
- POTENTIALLY_NOT_BREAKING,
- NOT_BREAKING

The algorithm analyses exported top-level statements (after inlining) in both rollups and tries to create a mapping between them by name.

The **BREAKING** outcome happens whenever the statement is:
- removed
- renamed
- changed
- not exported anymore (private)

The **POTENTIALLY_NOT_BREAKING** outcome  happens if it's not BREAKING and the new statement is added.

The **NOT_BREAKING** outcome happens if public API snapshot doesn't change.

Changelog:
[General][Added] - Add public JS API breaking change detection under `yarn diff-api-snapshot` script.

Pull Request resolved: https://github.com/facebook/react-native/pull/51972

Test Plan:
Signals, added tests.

In `react-native-github` run:
`yarn test scripts/diff-api-snapshot/__tests__/diffApiSnapshot-test.js`

Rollback Plan:

Reviewed By: j-piasecki

Differential Revision: D76430965

Pulled By: coado

fbshipit-source-id: 095a196aa4f643501db0af9262556ddefff5d30d
2025-06-17 02:27:53 -07:00

180 lines
5.5 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
*/
'use strict';
const {Result, diffApiSnapshot} = require('../diffApiSnapshot');
describe('diffApiSnapshot', () => {
test('should detect breaking change when a statement is deleted', () => {
const prevSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
export declare const DeletedExport: string;
`;
const newSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
`;
const res = diffApiSnapshot(prevSnapshot, newSnapshot);
expect(res.result).toBe(Result.BREAKING);
expect(res.changedApis).toEqual(['DeletedExport']);
});
test('should detect breaking change when a statement is changed', () => {
const prevSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
`;
const newSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_3; // Changed from AccessibilityInfo_2 to AccessibilityInfo_3
`;
const res = diffApiSnapshot(prevSnapshot, newSnapshot);
expect(res.result).toBe(Result.BREAKING);
expect(res.changedApis).toEqual(['AccessibilityInfo']);
});
test('should detect potentially not breaking change when a statement is added', () => {
const prevSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
`;
const newSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
export declare const NewExport: string; // New export added
`;
const res = diffApiSnapshot(prevSnapshot, newSnapshot);
expect(res.result).toBe(Result.POTENTIALLY_NON_BREAKING);
expect(res.changedApis).toEqual(['NewExport']);
});
test('should detect not breaking change when nothing is changed', () => {
const prevSnapshot = `
import * as React from 'react';
export declare type AccessibilityActionEvent = NativeSyntheticEvent<
Readonly<{
actionName: string;
}>
>;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
`;
const res = diffApiSnapshot(prevSnapshot, prevSnapshot);
expect(res.result).toBe(Result.NON_BREAKING);
expect(res.changedApis).toEqual([]);
});
test('should handle complex type declarations', () => {
const prevSnapshot = `
import * as React from 'react';
export declare type ComplexType = {
prop1: string;
prop2: number;
prop3: {
nestedProp1: boolean;
nestedProp2: Array<string>;
};
};
`;
const newSnapshot = `
import * as React from 'react';
export declare type ComplexType = {
prop1: string;
prop2: number;
prop3: {
nestedProp1: boolean;
nestedProp2: Array<string>;
nestedProp3: number; // Added property
};
};
`;
const res = diffApiSnapshot(prevSnapshot, newSnapshot);
expect(res.result).toBe(Result.BREAKING);
expect(res.changedApis).toEqual(['ComplexType']);
});
test('should handle interface declarations', () => {
const prevSnapshot = `
import * as React from 'react';
export interface TestInterface {
method1(): void;
property1: string;
}
`;
const newSnapshot = `
import * as React from 'react';
export interface TestInterface {
method1(): void;
property1: string;
method2(): number; // Added method
}
`;
const res = diffApiSnapshot(prevSnapshot, newSnapshot);
expect(res.result).toBe(Result.BREAKING);
expect(res.changedApis).toEqual(['TestInterface']);
});
test('should handle const and type of the same name', () => {
const prevSnapshot = `
import * as React from 'react';
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
export declare type AccessibilityInfo = typeof AccessibilityInfo;
`;
const newSnapshot = `
import * as React from 'react';
export declare type AccessibilityInfo = typeof AccessibilityInfo;
export declare const AccessibilityInfo: typeof AccessibilityInfo_2;
`;
const res = diffApiSnapshot(prevSnapshot, newSnapshot);
expect(res.result).toBe(Result.NON_BREAKING);
expect(res.changedApis).toEqual([]);
});
});