diff --git a/.github/actions/test-library-on-nightly/action.yml b/.github/actions/test-library-on-nightly/action.yml deleted file mode 100644 index 9001f8fac58..00000000000 --- a/.github/actions/test-library-on-nightly/action.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: test-library-on-nightly -description: Tests a library on a nightly -inputs: - library-npm-package: - description: The library npm package to add - required: true - platform: - description: whether we want to build for iOS or Android - required: true -runs: - using: composite - steps: - - name: Create new app - shell: bash - run: | - cd /tmp - npx @react-native-community/cli init RNApp --skip-install --version nightly - - name: Add library - shell: bash - run: | - cd /tmp/RNApp - yarn add ${{ inputs.library-npm-package }} - - # iOS - - name: Setup xcode - if: ${{ inputs.platform == 'ios' }} - uses: ./.github/actions/setup-xcode - - name: Build iOS - shell: bash - if: ${{ inputs.platform == 'ios' }} - run: | - cd /tmp/RNApp/ios - bundle install - RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1 bundle exec pod install - xcodebuild build \ - -workspace RNApp.xcworkspace \ - -scheme RNApp \ - -sdk iphonesimulator - - # Android - - name: Setup Java for Android - if: ${{ inputs.platform == 'android' }} - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'zulu' - - name: Build Android - shell: bash - if: ${{ inputs.platform == 'android' }} - run: | - cd /tmp/RNApp/android - ./gradlew assembleDebug diff --git a/.github/workflow-scripts/__tests__/firebaseUtils-test.js b/.github/workflow-scripts/__tests__/firebaseUtils-test.js deleted file mode 100644 index e12525c1602..00000000000 --- a/.github/workflow-scripts/__tests__/firebaseUtils-test.js +++ /dev/null @@ -1,895 +0,0 @@ -/** - * 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. - * - * @format - */ - -'use strict'; - -const { - FirebaseClient, - compareResults, - getYesterdayDate, - getTodayDate, -} = require('../firebaseUtils'); - -describe('FirebaseClient', () => { - const originalFetch = global.fetch; - const originalEnv = process.env; - - beforeEach(() => { - global.fetch = jest.fn(); - process.env = { - ...originalEnv, - FIREBASE_APP_EMAIL: 'test@example.com', - FIREBASE_APP_PASS: 'testpassword', - FIREBASE_APP_APIKEY: 'test-api-key', - FIREBASE_APP_PROJECTNAME: 'test-project', - }; - jest.spyOn(console, 'log').mockImplementation(() => {}); - jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - global.fetch = originalFetch; - process.env = originalEnv; - jest.restoreAllMocks(); - }); - - describe('constructor', () => { - it('should initialize with environment variables', () => { - const client = new FirebaseClient(); - expect(client.email).toBe('test@example.com'); - expect(client.password).toBe('testpassword'); - expect(client.apiKey).toBe('test-api-key'); - expect(client.projectId).toBe('test-project'); - expect(client.databaseUrl).toBe('test-project.firebaseio.com'); - expect(client.idToken).toBeNull(); - }); - }); - - describe('authenticate', () => { - it('should authenticate successfully', async () => { - const mockResponse = { - idToken: 'mock-id-token', - refreshToken: 'mock-refresh-token', - }; - - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResponse)), - }); - - const client = new FirebaseClient(); - await client.authenticate(); - - expect(client.idToken).toBe('mock-id-token'); - expect(global.fetch).toHaveBeenCalledWith( - 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=test-api-key', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: 'test@example.com', - password: 'testpassword', - returnSecureToken: true, - }), - }, - ); - }); - - it('should throw error when email is missing', async () => { - delete process.env.FIREBASE_APP_EMAIL; - const client = new FirebaseClient(); - - await expect(client.authenticate()).rejects.toThrow( - 'Firebase credentials not found in environment variables', - ); - }); - - it('should throw error when password is missing', async () => { - delete process.env.FIREBASE_APP_PASS; - const client = new FirebaseClient(); - - await expect(client.authenticate()).rejects.toThrow( - 'Firebase credentials not found in environment variables', - ); - }); - - it('should handle authentication failure', async () => { - global.fetch.mockResolvedValueOnce({ - ok: false, - status: 400, - text: jest.fn().mockResolvedValueOnce( - JSON.stringify({ - error: {message: 'Invalid credentials'}, - }), - ), - }); - - const client = new FirebaseClient(); - - await expect(client.authenticate()).rejects.toThrow( - 'HTTP 400: Invalid credentials', - ); - }); - }); - - describe('makeRequest', () => { - it('should make successful GET request', async () => { - const mockData = {test: 'data'}; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)), - }); - - const client = new FirebaseClient(); - const result = await client.makeRequest('example.com', '/test', 'GET'); - - expect(result).toEqual(mockData); - expect(global.fetch).toHaveBeenCalledWith('https://example.com/test', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - }); - - it('should make successful POST request with data', async () => { - const mockData = {success: true}; - const postData = {test: 'post data'}; - - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)), - }); - - const client = new FirebaseClient(); - const result = await client.makeRequest( - 'example.com', - '/test', - 'POST', - postData, - ); - - expect(result).toEqual(mockData); - expect(global.fetch).toHaveBeenCalledWith('https://example.com/test', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(postData), - }); - }); - - it('should handle non-JSON response', async () => { - const textResponse = 'plain text response'; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(textResponse), - }); - - const client = new FirebaseClient(); - const result = await client.makeRequest('example.com', '/test', 'GET'); - - expect(result).toBe(textResponse); - }); - - it('should handle HTTP error with JSON error message', async () => { - global.fetch.mockResolvedValueOnce({ - ok: false, - status: 404, - text: jest.fn().mockResolvedValueOnce( - JSON.stringify({ - error: {message: 'Not found'}, - }), - ), - }); - - const client = new FirebaseClient(); - - await expect( - client.makeRequest('example.com', '/test', 'GET'), - ).rejects.toThrow('HTTP 404: Not found'); - }); - - it('should handle HTTP error with plain text error message', async () => { - global.fetch.mockResolvedValueOnce({ - ok: false, - status: 500, - text: jest.fn().mockResolvedValueOnce('Internal Server Error'), - }); - - const client = new FirebaseClient(); - - await expect( - client.makeRequest('example.com', '/test', 'GET'), - ).rejects.toThrow('HTTP 500: Internal Server Error'); - }); - }); - - describe('makeDatabaseRequest', () => { - it('should make database request with existing token', async () => { - const mockData = {test: 'data'}; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.makeDatabaseRequest('2023-12-01', 'GET'); - - expect(result).toEqual(mockData); - expect(global.fetch).toHaveBeenCalledWith( - 'https://test-project.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token', - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - }); - - it('should authenticate before making request if no token exists', async () => { - const authResponse = {idToken: 'new-token'}; - const dataResponse = {test: 'data'}; - - global.fetch - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(authResponse)), - }) - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(dataResponse)), - }); - - const client = new FirebaseClient(); - const result = await client.makeDatabaseRequest('2023-12-01', 'GET'); - - expect(result).toEqual(dataResponse); - expect(global.fetch).toHaveBeenCalledTimes(2); - expect(client.idToken).toBe('new-token'); - }); - - it('should make PUT request with data', async () => { - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce('null'), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - const testData = [{library: 'test', status: 'success'}]; - - await client.makeDatabaseRequest('2023-12-01', 'PUT', testData); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://test-project.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token', - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(testData), - }, - ); - }); - }); - - describe('storeResults', () => { - it('should store results successfully', async () => { - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce('null'), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - const results = [{library: 'test', status: 'success'}]; - - await client.storeResults('2023-12-01', results); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://test-project.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token', - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(results), - }, - ); - expect(console.log).toHaveBeenCalledWith( - 'Successfully stored results for 2023-12-01', - ); - }); - }); - - describe('getResults', () => { - it('should retrieve results successfully', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const results = await client.getResults('2023-12-01'); - - expect(results).toEqual(mockResults); - }); - - it('should return null for 404 errors', async () => { - global.fetch.mockResolvedValueOnce({ - ok: false, - status: 404, - text: jest.fn().mockResolvedValueOnce('Not Found'), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const results = await client.getResults('2023-12-01'); - - expect(results).toBeNull(); - }); - - it('should throw error for non-404 HTTP errors', async () => { - global.fetch.mockResolvedValueOnce({ - ok: false, - status: 500, - text: jest.fn().mockResolvedValueOnce('Internal Server Error'), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - await expect(client.getResults('2023-12-01')).rejects.toThrow( - 'HTTP 500: Internal Server Error', - ); - }); - }); - - describe('getLatestResults', () => { - it('should authenticate before making requests if no token exists', async () => { - const authResponse = {idToken: 'new-token'}; - const mockResults = [{library: 'test', status: 'success'}]; - - global.fetch - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(authResponse)), - }) - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - const result = await client.getLatestResults('2023-12-15', 1); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-14', - }); - expect(client.idToken).toBe('new-token'); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-14 (1 days back)', - ); - }); - - it('should find results from the previous day', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15', 7); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-14', - }); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-14 (1 days back)', - ); - }); - - it('should find results from several days back', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - - // Mock 404 responses for first 2 days, then success on 3rd day - global.fetch - .mockResolvedValueOnce({ - ok: false, - status: 404, - text: jest.fn().mockResolvedValueOnce('Not Found'), - }) - .mockResolvedValueOnce({ - ok: false, - status: 404, - text: jest.fn().mockResolvedValueOnce('Not Found'), - }) - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15', 7); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-12', - }); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-13 (2 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-12 (3 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-12 (3 days back)', - ); - }); - - it('should skip empty results and continue searching', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - - // Mock empty array for first day, then valid results on second day - global.fetch - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify([])), - }) - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15', 7); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-13', - }); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-13 (2 days back)...', - ); - expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-13 (2 days back)', - ); - }); - - it('should return null when no results found within maxDaysBack', async () => { - // Mock 404 responses for all days - global.fetch.mockResolvedValue({ - ok: false, - status: 404, - text: jest.fn().mockResolvedValue('Not Found'), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15', 3); - - expect(result).toEqual({ - results: null, - date: null, - }); - expect(console.log).toHaveBeenCalledWith( - 'No previous results found within the last 3 days', - ); - expect(global.fetch).toHaveBeenCalledTimes(3); - }); - - it('should use default maxDaysBack of 7 when not specified', async () => { - // Mock 404 responses for all days - global.fetch.mockResolvedValue({ - ok: false, - status: 404, - text: jest.fn().mockResolvedValue('Not Found'), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15'); - - expect(result).toEqual({ - results: null, - date: null, - }); - expect(console.log).toHaveBeenCalledWith( - 'No previous results found within the last 7 days', - ); - expect(global.fetch).toHaveBeenCalledTimes(7); - }); - - it('should handle non-404 errors and continue searching', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - - // Mock 500 error for first day, then success on second day - global.fetch - .mockResolvedValueOnce({ - ok: false, - status: 500, - text: jest.fn().mockResolvedValueOnce('Internal Server Error'), - }) - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15', 7); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-13', - }); - expect(console.log).toHaveBeenCalledWith( - 'No results found for 2023-12-14: HTTP 500: Internal Server Error', - ); - expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-13 (2 days back)', - ); - }); - - it('should handle date boundaries correctly', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - // Test month boundary - const result = await client.getLatestResults('2023-12-01', 1); - - expect(result).toEqual({ - results: mockResults, - date: '2023-11-30', - }); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-11-30 (1 days back)...', - ); - }); - - it('should handle year boundary correctly', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - global.fetch.mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - // Test year boundary - const result = await client.getLatestResults('2024-01-01', 1); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-31', - }); - expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-31 (1 days back)...', - ); - }); - - it('should handle null results and continue searching', async () => { - const mockResults = [{library: 'test', status: 'success'}]; - - // Mock null for first day, then valid results on second day - global.fetch - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce('null'), - }) - .mockResolvedValueOnce({ - ok: true, - text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), - }); - - const client = new FirebaseClient(); - client.idToken = 'existing-token'; - - const result = await client.getLatestResults('2023-12-15', 7); - - expect(result).toEqual({ - results: mockResults, - date: '2023-12-13', - }); - }); - }); -}); - -describe('compareResults', () => { - it('should handle null previous results', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const result = compareResults(currentResults, null); - - expect(result).toEqual({ - broken: [], - recovered: [], - newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}], - }); - }); - - it('should handle undefined previous results', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - ]; - - const result = compareResults(currentResults, undefined); - - expect(result).toEqual({ - broken: [], - recovered: [], - newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}], - }); - }); - - it('should identify broken tests', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([ - { - library: 'lib1', - platform: 'iOS', - previousStatus: 'success', - currentStatus: 'failed', - }, - ]); - expect(result.recovered).toEqual([]); - }); - - it('should identify recovered tests', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([]); - expect(result.recovered).toEqual([ - { - library: 'lib1', - platform: 'iOS', - previousStatus: 'failed', - currentStatus: 'success', - }, - ]); - }); - - it('should identify both broken and recovered tests', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - {library: 'lib3', platform: 'iOS', status: 'success'}, - ]; - - const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'failed'}, - {library: 'lib3', platform: 'iOS', status: 'success'}, - ]; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([ - { - library: 'lib1', - platform: 'iOS', - previousStatus: 'success', - currentStatus: 'failed', - }, - ]); - expect(result.recovered).toEqual([ - { - library: 'lib2', - platform: 'Android', - previousStatus: 'failed', - currentStatus: 'success', - }, - ]); - }); - - it('should handle tests that are not in previous results', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - ]; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([ - { - library: 'lib1', - platform: 'iOS', - previousStatus: 'success', - currentStatus: 'failed', - }, - ]); - expect(result.recovered).toEqual([]); - }); - - it('should handle empty current results', () => { - const currentResults = []; - const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - ]; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([]); - expect(result.recovered).toEqual([]); - }); - - it('should handle empty previous results', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - ]; - const previousResults = []; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([]); - expect(result.recovered).toEqual([]); - // When previousResults is an empty array (not null/undefined), - // the function doesn't return newFailures property - expect(result.newFailures).toBeUndefined(); - }); - - it('should handle different status values', () => { - const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'timeout'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - ]; - - const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'error'}, - ]; - - const result = compareResults(currentResults, previousResults); - - expect(result.broken).toEqual([ - { - library: 'lib1', - platform: 'iOS', - previousStatus: 'success', - currentStatus: 'timeout', - }, - ]); - expect(result.recovered).toEqual([ - { - library: 'lib2', - platform: 'Android', - previousStatus: 'error', - currentStatus: 'success', - }, - ]); - }); -}); - -describe('getYesterdayDate', () => { - it("should return yesterday's date in YYYY-MM-DD format", () => { - const mockDate = new Date('2023-12-15T10:30:00Z'); - jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - const result = getYesterdayDate(); - - expect(result).toBe('2023-12-14'); - - global.Date.mockRestore(); - }); - - it('should handle month boundary correctly', () => { - const mockDate = new Date('2023-12-01T10:30:00Z'); - jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - const result = getYesterdayDate(); - - expect(result).toBe('2023-11-30'); - - global.Date.mockRestore(); - }); - - it('should handle year boundary correctly', () => { - const mockDate = new Date('2024-01-01T10:30:00Z'); - jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - const result = getYesterdayDate(); - - expect(result).toBe('2023-12-31'); - - global.Date.mockRestore(); - }); -}); - -describe('getTodayDate', () => { - it("should return today's date in YYYY-MM-DD format", () => { - const mockDate = new Date('2023-12-15T10:30:00Z'); - jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - const result = getTodayDate(); - - expect(result).toBe('2023-12-15'); - - global.Date.mockRestore(); - }); - - it('should handle different times of day correctly', () => { - const mockDate = new Date('2023-12-15T23:59:59Z'); - jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - const result = getTodayDate(); - - expect(result).toBe('2023-12-15'); - - global.Date.mockRestore(); - }); -}); diff --git a/.github/workflow-scripts/__tests__/notifyDiscord-test.js b/.github/workflow-scripts/__tests__/notifyDiscord-test.js deleted file mode 100644 index c87a293e20c..00000000000 --- a/.github/workflow-scripts/__tests__/notifyDiscord-test.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * 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. - * - * @format - */ - -'use strict'; - -const { - prepareFailurePayload, - sendMessageToDiscord, -} = require('../notifyDiscord'); - -describe('prepareFailurePayload', () => { - it('should handle undefined failures', () => { - const message = prepareFailurePayload(undefined); - expect(message).toEqual({ - content: - '⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.', - }); - }); - - it('should handle empty failures array', () => { - const message = prepareFailurePayload([]); - expect(message).toEqual({ - content: - '⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.', - }); - }); - - it('should format a single failure correctly', () => { - const failures = [ - { - library: 'react-native-reanimated', - platform: 'iOS', - }, - ]; - - const message = prepareFailurePayload(failures); - expect(message).toEqual({ - content: - '⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n❌ [iOS] react-native-reanimated', - }); - }); - - it('should sort multiple failures by platform and library name', () => { - const failures = [ - { - library: 'react-native-reanimated', - platform: 'iOS', - }, - { - library: 'react-native-gesture-handler', - platform: 'Android', - }, - { - library: 'react-native-screens', - platform: 'iOS', - }, - { - library: 'react-native-svg', - platform: 'Android', - }, - ]; - - const message = prepareFailurePayload(failures); - - // The failures should be sorted: first Android (alphabetically), then iOS - // Within each platform, libraries should be sorted alphabetically - expect(message).toEqual({ - content: - '⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n❌ [Android] react-native-gesture-handler\n❌ [Android] react-native-svg\n❌ [iOS] react-native-reanimated\n❌ [iOS] react-native-screens', - }); - }); - - it('should handle failures with missing properties', () => { - const failures = [ - { - // Missing library - platform: 'iOS', - }, - { - library: 'react-native-gesture-handler', - // Missing platform - }, - { - // Both missing - }, - ]; - - const message = prepareFailurePayload(failures); - - expect(message).toEqual({ - content: - '⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n❌ [iOS] Unknown\n❌ [Unknown] react-native-gesture-handler\n❌ [Unknown] Unknown', - }); - }); -}); - -describe('sendMessageToDiscord', () => { - // Store the original fetch function - const originalFetch = global.fetch; - - // Setup and teardown for each test - beforeEach(() => { - // Mock the global fetch function - global.fetch = jest.fn(); - // Silence console logs during tests - jest.spyOn(console, 'log').mockImplementation(() => {}); - jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - // Restore the original fetch function - global.fetch = originalFetch; - // Restore console functions - jest.restoreAllMocks(); - }); - - it('should throw an error if webhook URL is missing', async () => { - await expect(sendMessageToDiscord(null, {})).rejects.toThrow( - 'Discord webhook URL is missing', - ); - }); - - it('should send a message successfully', async () => { - // Mock a successful response - global.fetch.mockResolvedValueOnce({ - ok: true, - status: 200, - }); - - const webhook = 'https://discord.com/api/webhooks/123/abc'; - const message = {content: 'Test message'}; - - await expect(sendMessageToDiscord(webhook, message)).resolves.not.toThrow(); - - // Verify fetch was called with the right arguments - expect(global.fetch).toHaveBeenCalledWith(webhook, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(message), - }); - - // Verify console.log was called - expect(console.log).toHaveBeenCalledWith( - 'Successfully sent message to Discord', - ); - }); - - it('should throw an error if the response is not ok', async () => { - // Mock a failed response - global.fetch.mockResolvedValueOnce({ - ok: false, - status: 400, - text: jest.fn().mockResolvedValueOnce('Bad Request'), - }); - - const webhook = 'https://discord.com/api/webhooks/123/abc'; - const message = {content: 'Test message'}; - - await expect(sendMessageToDiscord(webhook, message)).rejects.toThrow( - 'HTTP status code: 400', - ); - - // Verify console.error was called - expect(console.error).toHaveBeenCalledWith( - 'Failed to send message to Discord: 400 Bad Request', - ); - }); - - it('should throw an error if fetch fails', async () => { - // Mock a network error - const networkError = new Error('Network error'); - global.fetch.mockRejectedValueOnce(networkError); - - const webhook = 'https://discord.com/api/webhooks/123/abc'; - const message = {content: 'Test message'}; - - await expect(sendMessageToDiscord(webhook, message)).rejects.toThrow( - 'Network error', - ); - }); -}); diff --git a/.github/workflow-scripts/collectNightlyOutcomes.js b/.github/workflow-scripts/collectNightlyOutcomes.js deleted file mode 100644 index 25d0a8a0edd..00000000000 --- a/.github/workflow-scripts/collectNightlyOutcomes.js +++ /dev/null @@ -1,174 +0,0 @@ -/** - * 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. - * - * @format - */ - -const fs = require('fs'); -const path = require('path'); -const { - prepareFailurePayload, - prepareComparisonPayload, - sendMessageToDiscord, -} = require('./notifyDiscord'); -const { - FirebaseClient, - compareResults, - getYesterdayDate, - getTodayDate, -} = require('./firebaseUtils'); - -function readOutcomes() { - const baseDir = '/tmp'; - let outcomes = []; - fs.readdirSync(baseDir).forEach(file => { - const fullPath = path.join(baseDir, file); - if (fullPath.endsWith('outcome') && fs.statSync(fullPath).isDirectory) { - fs.readdirSync(fullPath).forEach(subFile => { - const subFullPath = path.join(fullPath, subFile); - if (subFullPath.endsWith('outcome')) { - const [library, status] = String(fs.readFileSync(subFullPath, 'utf8')) - .trim() - .split(':'); - const platform = subFile.includes('android') ? 'Android' : 'iOS'; - console.log( - `[${platform}] ${library} completed with status ${status}`, - ); - outcomes.push({ - library: library.trim(), - platform, - status: status.trim(), - }); - } - }); - } else if (fullPath.endsWith('outcome')) { - const [library, status] = String(fs.readFileSync(fullPath, 'utf8')) - .trim() - .split(':'); - const platform = file.includes('android') ? 'Android' : 'iOS'; - console.log(`[${platform}] ${library} completed with status ${status}`); - outcomes.push({ - library: library.trim(), - platform, - status: status.trim(), - }); - } - }); - return outcomes; -} - -function printFailures(outcomes) { - console.log('Printing failures...'); - let failedLibraries = []; - outcomes.forEach(entry => { - if (entry.status !== 'success') { - console.log( - `❌ [${entry.platform}] ${entry.library} failed with status ${entry.status}`, - ); - failedLibraries.push({ - library: entry.library, - platform: entry.platform, - }); - } - }); - return failedLibraries; -} - -/** - * Sends a message to Discord with the list of failures. - * @param {string} webHook - The Discord webhook URL - * @param {Array} failures - List of failures to report - * @returns {Promise} - A promise that resolves when the message is sent - */ -async function notifyDiscord(webHook, failures) { - if (!webHook) { - console.error('Discord webhook URL is missing'); - return; - } - - if (!failures || failures.length === 0) { - console.log('No failures to report to Discord'); - return; - } - - try { - // Use the prepareFailurePayload function to format the message - const message = prepareFailurePayload(failures); - - // Use the sendMessageToDiscord function to send the message - await sendMessageToDiscord(webHook, message); - } catch (error) { - console.error('Error in notifyDiscord function:', error); - throw error; - } -} - -async function collectResults(discordWebHook) { - const outcomes = readOutcomes(); - const failures = printFailures(outcomes); - - // Send failure notification if there are current failures - if (failures.length > 0) { - if (discordWebHook) { - console.log('Sending current failures to Discord...'); - await notifyDiscord(discordWebHook, failures); - } else { - console.log('Discord webhook not set'); - } - } - - // Initialize Firebase client - const firebaseClient = new FirebaseClient(); - const today = getTodayDate(); - - try { - // Store today's results in Firebase - console.log(`Storing results for ${today} in Firebase...`); - await firebaseClient.storeResults(today, outcomes); - - // Get the most recent previous results for comparison - console.log(`Looking for most recent previous results before ${today}...`); - const {results: previousResults, date: previousDate} = - await firebaseClient.getLatestResults(today); - - let broken = []; - let recovered = []; - - if (previousResults) { - console.log(`Comparing with results from ${previousDate}`); - // Compare results and identify broken/recovered jobs - const comparison = compareResults(outcomes, previousResults); - broken = comparison.broken; - recovered = comparison.recovered; - - console.log( - `Found ${broken.length} newly broken jobs and ${recovered.length} recovered jobs compared to ${previousDate}`, - ); - } else { - console.log( - 'No previous results found for comparison - this might be the first run or no recent data available', - ); - } - - // Send comparison message to Discord if there are changes - if (discordWebHook && (broken.length > 0 || recovered.length > 0)) { - console.log('Sending comparison results to Discord...'); - const comparisonMessage = prepareComparisonPayload(broken, recovered); - await sendMessageToDiscord(discordWebHook, comparisonMessage); - } - - console.log('✅ All tests passed!'); - } catch (error) { - console.error('Error in collectResults:', error); - // If Firebase fails but there are no test failures, don't fail the workflow - console.log('⚠️ Firebase operations failed, but all tests passed'); - } -} - -module.exports = { - collectResults, - notifyDiscord, -}; diff --git a/.github/workflow-scripts/notifyDiscord.js b/.github/workflow-scripts/notifyDiscord.js deleted file mode 100644 index 618dbb996e7..00000000000 --- a/.github/workflow-scripts/notifyDiscord.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * 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. - * - * @format - */ - -/** - * Sends a message to Discord using the webhook URL. - * @param {string} webHook - The Discord webhook URL - * @param {Object} message - The message to send - * @returns {Promise} - A promise that resolves when the message is sent - */ -async function sendMessageToDiscord(webHook, message) { - if (!webHook) { - throw new Error('Discord webhook URL is missing'); - } - - // Send the request using fetch - const response = await fetch(webHook, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(message), - }); - - // Handle the response - if (response.ok) { - console.log('Successfully sent message to Discord'); - return; - } else { - const errorText = await response.text(); - console.error( - `Failed to send message to Discord: ${response.status} ${errorText}`, - ); - throw new Error(`HTTP status code: ${response.status}`); - } -} - -/** - * Sorts jobs by platform first, then by library name. - * @param {Array} jobs - Array of jobs with platform and library properties - * @returns {Array} - Sorted array of jobs - */ -function sortResultsByPlatformAndLibrary(jobs) { - return [...jobs].sort((a, b) => { - // First sort by platform - const platformA = a.platform || 'Unknown'; - const platformB = b.platform || 'Unknown'; - - if (platformA !== platformB) { - return platformA.localeCompare(platformB); - } - - // Then sort by library name - const libraryA = a.library || 'Unknown'; - const libraryB = b.library || 'Unknown'; - return libraryA.localeCompare(libraryB); - }); -} - -/** - * Prepares a formatted Discord message payload from a list of failures. - * @param {Array} failures - List of failures to format - * @returns {Object} - The formatted Discord message payload - */ -function prepareFailurePayload(failures) { - if (!failures || failures.length === 0) { - return { - content: - '⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.', - }; - } - - // Sort failures by platform and then by library name - const sortedFailures = sortResultsByPlatformAndLibrary(failures); - - // Format the failures into a message - const formattedFailures = sortedFailures - .map(failure => { - const library = failure.library || 'Unknown'; - const platform = failure.platform || 'Unknown'; - return `❌ [${platform}] ${library}`; - }) - .join('\n'); - - return { - content: `⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n${formattedFailures}`, - }; -} - -/** - * Prepares a formatted Discord message payload for broken and recovered nightly jobs. - * @param {Array} broken - List of newly broken jobs - * @param {Array} recovered - List of recovered jobs - * @returns {Object} - The formatted Discord message payload - */ -function prepareComparisonPayload(broken, recovered) { - let content = '📊 **React Native Nightly Integration Status Update** 📊\n\n'; - - if (broken.length === 0 && recovered.length === 0) { - content += - 'No changes from yesterday - all nightly jobs maintained their previous status.'; - } else { - if (broken.length > 0) { - content += '🔴 **Newly Broken Jobs:**\n'; - const sortedBroken = sortResultsByPlatformAndLibrary(broken); - - sortedBroken.forEach(job => { - content += `❌ [${job.platform}] ${job.library} (was ${job.previousStatus}, now ${job.currentStatus})\n`; - }); - content += '\n'; - } - - if (recovered.length > 0) { - content += '🟢 **Recovered Jobs:**\n'; - const sortedRecovered = sortResultsByPlatformAndLibrary(recovered); - - sortedRecovered.forEach(job => { - content += `✅ [${job.platform}] ${job.library} (was ${job.previousStatus}, now ${job.currentStatus})\n`; - }); - } - } - - return {content}; -} - -// Export the functions using CommonJS syntax -module.exports = { - prepareFailurePayload, - prepareComparisonPayload, - sendMessageToDiscord, -}; diff --git a/.github/workflows/test-libraries-on-nightlies.yml b/.github/workflows/test-libraries-on-nightlies.yml deleted file mode 100644 index b827ebd21f4..00000000000 --- a/.github/workflows/test-libraries-on-nightlies.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Test Libraries on Nightlies - -on: - workflow_call: - secrets: - discord_webhook_url: - required: true - firebase_app_email: - required: true - firebase_app_pass: - required: true - firebase_app_apikey: - required: true - firebase_app_projectname: - required: true - - -# We use the matrix.library entry to specify the dependency we want to use -# The key is used directly as the in the `yarn add ` command. -jobs: - runner-setup: - runs-on: ubuntu-latest - outputs: - runners: '{"ios":"macos-14-large", "android": "ubuntu-latest"}' - steps: - - run: echo no-op - - test-library-on-nightly: - name: "[${{ matrix.platform }}] ${{ matrix.library }}" - needs: runner-setup - runs-on: ${{ fromJSON(needs.runner-setup.outputs.runners)[matrix.platform] }} - continue-on-error: true - strategy: - matrix: - library: [ - "react-native-async-storage", - "react-native-blob-util", - "@react-native-clipboard/clipboard", - "@react-native-community/datetimepicker", - "react-native-gesture-handler", - "react-native-image-picker", - "react-native-linear-gradient", - "@react-native-masked-view/masked-view", - # "react-native-maps", React Native Maps with the New Arch support has a complex cocoapods setup for iOS. It needs a dedicated workflow. - "@react-native-community/netinfo", - "react-native-reanimated@nightly react-native-worklets@nightly", #reanimated requires worklet to be explicitly installed as a separate package - "react-native-svg", - "react-native-video", - "react-native-webview", - "react-native-mmkv", - "react-native-screens", - "react-native-pager-view", - "@react-native-community/slider", - # additional OSS libs used internally - "scandit-react-native-datacapture-barcode scandit-react-native-datacapture-core", - "react-native-contacts", - "react-native-device-info", - "react-native-email-link", - "@dr.pogodin/react-native-fs", - "react-native-permissions", - "react-native-vector-icons", - "react-native-masked-view", - "@react-native-community/image-editor", - ] - platform: [ios, android] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Node.js - uses: ./.github/actions/setup-node - - name: Test ${{ matrix.library }} - id: run-test - uses: ./.github/actions/test-library-on-nightly - with: - library-npm-package: ${{ matrix.library }} - platform: ${{ matrix.platform}} - - name: Save outcome - id: save-outcome - if: always() - run: | - LIB_FOLDER=$(echo "${{matrix.library}}" | tr ' ' '_' | tr '/' '_') - echo "${{matrix.library}}: ${{steps.run-test.outcome}}" > "/tmp/$LIB_FOLDER-${{ matrix.platform }}-outcome" - echo "lib_folder=$LIB_FOLDER" >> $GITHUB_OUTPUT - - name: Upload Artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.save-outcome.outputs.lib_folder }}-${{ matrix.platform }}-outcome - path: /tmp/${{ steps.save-outcome.outputs.lib_folder }}-${{ matrix.platform }}-outcome - - - collect-results: - runs-on: ubuntu-latest - needs: [test-library-on-nightly] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Restore outcomes - uses: actions/download-artifact@v4 - with: - pattern: '*-outcome' - path: /tmp - - name: Collect failures - uses: actions/github-script@v6 - env: - FIREBASE_APP_EMAIL: ${{ secrets.firebase_app_email }} - FIREBASE_APP_PASS: ${{ secrets.firebase_app_pass }} - FIREBASE_APP_APIKEY: ${{ secrets.firebase_app_apikey }} - FIREBASE_APP_PROJECTNAME: ${{ secrets.firebase_app_projectname }} - with: - script: | - const {collectResults} = require('./.github/workflow-scripts/collectNightlyOutcomes.js'); - await collectResults('${{secrets.discord_webhook_url}}');