diff --git a/package.json b/package.json index ffdabe09bc611d..6113aa3b5ebe79 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "prettier": "2.8.8", "prettier-plugin-hermes-parser": "0.25.1", "react": "19.0.0", - "react-test-renderer": "19.0.0", + "rimraf": "^3.0.2", "shelljs": "^0.8.5", "signedsource": "^1.0.0", diff --git a/packages/react-native/Libraries/Blob/URL.js b/packages/react-native/Libraries/Blob/URL.js index db4bb2792a1e3a..24b06cf551bb49 100644 --- a/packages/react-native/Libraries/Blob/URL.js +++ b/packages/react-native/Libraries/Blob/URL.js @@ -9,29 +9,27 @@ */ import type Blob from './Blob'; - import NativeBlobModule from './NativeBlobModule'; let BLOB_URL_PREFIX = null; +// Initialize BLOB_URL_PREFIX if NativeBlobModule is available and properly configured if ( NativeBlobModule && typeof NativeBlobModule.getConstants().BLOB_URI_SCHEME === 'string' ) { const constants = NativeBlobModule.getConstants(); - // $FlowFixMe[incompatible-type] asserted above - // $FlowFixMe[unsafe-addition] BLOB_URL_PREFIX = constants.BLOB_URI_SCHEME + ':'; if (typeof constants.BLOB_URI_HOST === 'string') { - BLOB_URL_PREFIX += `//${constants.BLOB_URI_HOST}/`; + BLOB_URL_PREFIX += //${constants.BLOB_URI_HOST}/; } } /* - * To allow Blobs be accessed via `content://` URIs, - * you need to register `BlobProvider` as a ContentProvider in your app's `AndroidManifest.xml`: + * To allow Blobs be accessed via content:// URIs, + * you need to register BlobProvider as a ContentProvider in your app's AndroidManifest.xml: * - * ```xml + * xml * * * * * - * ``` - * And then define the `blob_provider_authority` string in `res/values/strings.xml`. + * + * And then define the blob_provider_authority string in res/values/strings.xml. * Use a dotted name that's entirely unique to your app: * - * ```xml + * xml * * your.app.package.blobs * - * ``` + * */ export {URLSearchParams} from './URLSearchParams'; -function validateBaseUrl(url: string) { +// Validate the base URL with a regular expression +function validateBaseUrl(url: string): boolean { // from this MIT-licensed gist: https://gist.github.com/dperini/729294 - return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test( + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S)?$/.test( url, ); } @@ -64,21 +63,26 @@ function validateBaseUrl(url: string) { export class URL { _url: string; _searchParamsInstance: ?URLSearchParams = null; + _urlObject: URL; static createObjectURL(blob: Blob): string { if (BLOB_URL_PREFIX === null) { - throw new Error('Cannot create URL for blob!'); + throw new Error('Cannot create URL for blob! Ensure NativeBlobModule is properly configured.'); + } + if (!blob || !blob.data || !blob.data.blobId) { + throw new Error('Invalid blob data: Missing blobId or data.'); } - return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${blob.data.offset}&size=${blob.size}`; + return ${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${blob.data.offset}&size=${blob.size}; } static revokeObjectURL(url: string) { - // Do nothing. + // Do nothing, no implementation needed for revoking Blob URLs in this case. } - // $FlowFixMe[missing-local-annot] constructor(url: string, base: string | URL) { let baseUrl = null; + + // Validate URL format and handle base URL correctly if (!base || validateBaseUrl(url)) { this._url = url; if (!this._url.endsWith('/')) { @@ -88,34 +92,39 @@ export class URL { if (typeof base === 'string') { baseUrl = base; if (!validateBaseUrl(baseUrl)) { - throw new TypeError(`Invalid base URL: ${baseUrl}`); + throw new TypeError(Invalid base URL: ${baseUrl}); } } else { baseUrl = base.toString(); } + + // Ensure correct URL formatting if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); } if (!url.startsWith('/')) { - url = `/${url}`; + url = /${url}; } if (baseUrl.endsWith(url)) { url = ''; } - this._url = `${baseUrl}${url}`; + this._url = ${baseUrl}${url}; } + + // Create a URL object for easier parsing (browser-like behavior) + this._urlObject = new globalThis.URL(this._url); } get hash(): string { - throw new Error('URL.hash is not implemented'); + return this._urlObject.hash || ''; // Extract the fragment part } get host(): string { - throw new Error('URL.host is not implemented'); + return this._urlObject.host || ''; // Extracts the host and port (if present) } get hostname(): string { - throw new Error('URL.hostname is not implemented'); + return this._urlObject.hostname || ''; // Extracts just the hostname (without port) } get href(): string { @@ -123,32 +132,33 @@ export class URL { } get origin(): string { - throw new Error('URL.origin is not implemented'); + return this._urlObject.origin || ''; // Extracts the origin (protocol + hostname + port) } get password(): string { - throw new Error('URL.password is not implemented'); + const match = this._urlObject.href.match(/^.:\/\/(.):(.*)@/); + return match && match[2] ? match[2] : ''; // Extract password from "username:password" part } get pathname(): string { - throw new Error('URL.pathname not implemented'); + return this._urlObject.pathname || ''; // Extracts the pathname (e.g., "/path/to/resource") } get port(): string { - throw new Error('URL.port is not implemented'); + return this._urlObject.port || ''; // Extracts the port part } get protocol(): string { - throw new Error('URL.protocol is not implemented'); + return this._urlObject.protocol || ''; // Extracts the protocol (e.g., "http:" or "https:") } get search(): string { - throw new Error('URL.search is not implemented'); + return this._urlObject.search || ''; // Extracts the query string (e.g., "?id=123") } get searchParams(): URLSearchParams { if (this._searchParamsInstance == null) { - this._searchParamsInstance = new URLSearchParams(); + this._searchParamsInstance = new URLSearchParams(this._urlObject.search); } return this._searchParamsInstance; } @@ -161,13 +171,13 @@ export class URL { if (this._searchParamsInstance === null) { return this._url; } - // $FlowFixMe[incompatible-use] const instanceString = this._searchParamsInstance.toString(); const separator = this._url.indexOf('?') > -1 ? '&' : '?'; return this._url + separator + instanceString; } get username(): string { - throw new Error('URL.username is not implemented'); + const match = this._urlObject.href.match(/^.:\/\/(.?)(?::(.*))?@/); + return match && match[1] ? match[1] : ''; // Extract username from "username:password" part } -} +} \ No newline at end of file diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js index 1b39b40ddc10d5..24aca736c3adf3 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js @@ -18,7 +18,7 @@ import * as React from 'react'; const ExceptionsManager = require('../../Core/ExceptionsManager.js'); const LogBoxData = require('../Data/LogBoxData'); -const TestRenderer = require('react-test-renderer'); +import { create } from 'react-native-fantom'; const installLogBox = () => { const LogBox = require('../LogBox').default; @@ -41,10 +41,8 @@ describe('LogBox', () => { jest.resetModules(); jest.restoreAllMocks(); jest.spyOn(console, 'error').mockImplementation(() => {}); - mockError.mockClear(); mockWarn.mockClear(); - // Reset ExceptionManager patching. if (console._errorOriginal) { console._errorOriginal = null; } @@ -65,16 +63,14 @@ describe('LogBox', () => { // as it is. it.skip('integrates with React and handles a key error in LogBox', () => { const spy = jest.spyOn(LogBoxData, 'addLog'); - installLogBox(); + // Spy console.error after LogBox is installed // so we can assert on what React logs. jest.spyOn(console, 'error'); - + installLogBox(); let output; - TestRenderer.act(() => { - output = TestRenderer.create(); - }); + create(); // The key error should always be the highest severity. // In LogBox, we expect these errors to: @@ -126,16 +122,14 @@ describe('LogBox', () => { // as it is. it.skip('integrates with React and handles a fragment warning in LogBox', () => { const spy = jest.spyOn(LogBoxData, 'addLog'); - installLogBox(); + // Spy console.error after LogBox is installed // so we can assert on what React logs. jest.spyOn(console, 'error'); - + installLogBox(); let output; - TestRenderer.act(() => { - output = TestRenderer.create(); - }); + create(); // The fragment warning is not as severe. For this warning we don't want to // pop open a dialog, so we show a collapsed error UI. @@ -188,9 +182,7 @@ describe('LogBox', () => { jest.spyOn(console, 'error'); let output; - TestRenderer.act(() => { - output = TestRenderer.create(); - }); + create(); // Manual console errors should show a collapsed error dialog. // When there is no component stack, we expect these errors to: @@ -230,9 +222,8 @@ describe('LogBox', () => { jest.spyOn(console, 'error'); let output; - TestRenderer.act(() => { - output = TestRenderer.create(); - }); + create(); + // Manual console errors should show a collapsed error dialog. // When there is a component stack, we expect these errors to: