/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import JSON5 from 'json5'; import type {Element} from './views/Components/types'; import type {StateContext} from './views/Components/TreeContext'; import type Store from './store'; export function printElement(element: Element, includeWeight: boolean = false) { let prefix = ' '; if (element.children.length > 0) { prefix = element.isCollapsed ? '▸' : '▾'; } let key = ''; if (element.key !== null) { key = ` key="${element.key}"`; } let hocDisplayNames = null; if (element.hocDisplayNames !== null) { hocDisplayNames = [...element.hocDisplayNames]; } const hocs = hocDisplayNames === null ? '' : ` [${hocDisplayNames.join('][')}]`; let suffix = ''; if (includeWeight) { suffix = ` (${element.isCollapsed ? 1 : element.weight})`; } return `${' '.repeat(element.depth + 1)}${prefix} <${element.displayName || 'null'}${key}>${hocs}${suffix}`; } export function printOwnersList( elements: Array, includeWeight: boolean = false, ) { return elements .map(element => printElement(element, includeWeight)) .join('\n'); } export function printStore( store: Store, includeWeight: boolean = false, state: StateContext | null = null, ) { const snapshotLines = []; let rootWeight = 0; function printSelectedMarker(index: number): string { if (state === null) { return ''; } return state.selectedElementIndex === index ? `→` : ' '; } function printErrorsAndWarnings(element: Element): string { const { errorCount, warningCount, } = store.getErrorAndWarningCountForElementID(element.id); if (errorCount === 0 && warningCount === 0) { return ''; } return ` ${errorCount > 0 ? '✕' : ''}${warningCount > 0 ? '⚠' : ''}`; } const ownerFlatTree = state !== null ? state.ownerFlatTree : null; if (ownerFlatTree !== null) { snapshotLines.push( '[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''), ); ownerFlatTree.forEach((element, index) => { const printedSelectedMarker = printSelectedMarker(index); const printedElement = printElement(element, false); const printedErrorsAndWarnings = printErrorsAndWarnings(element); snapshotLines.push( `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`, ); }); } else { const errorsAndWarnings = store._errorsAndWarnings; if (errorsAndWarnings.size > 0) { let errorCount = 0; let warningCount = 0; errorsAndWarnings.forEach(entry => { errorCount += entry.errorCount; warningCount += entry.warningCount; }); snapshotLines.push(`✕ ${errorCount}, ⚠ ${warningCount}`); } store.roots.forEach(rootID => { const {weight} = ((store.getElementByID(rootID): any): Element); const maybeWeightLabel = includeWeight ? ` (${weight})` : ''; // Store does not (yet) expose a way to get errors/warnings per root. snapshotLines.push(`[root]${maybeWeightLabel}`); for (let i = rootWeight; i < rootWeight + weight; i++) { const element = store.getElementAtIndex(i); if (element == null) { throw Error(`Could not find element at index ${i}`); } const printedSelectedMarker = printSelectedMarker(i); const printedElement = printElement(element, includeWeight); const printedErrorsAndWarnings = printErrorsAndWarnings(element); snapshotLines.push( `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`, ); } rootWeight += weight; }); // Make sure the pretty-printed test align with the Store's reported number of total rows. if (rootWeight !== store.numElements) { throw Error( `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`, ); } // If roots have been unmounted, verify that they've been removed from maps. // This helps ensure the Store doesn't leak memory. store.assertExpectedRootMapSizes(); } return snapshotLines.join('\n'); } // We use JSON.parse to parse string values // e.g. 'foo' is not valid JSON but it is a valid string // so this method replaces e.g. 'foo' with "foo" export function sanitizeForParse(value: any) { if (typeof value === 'string') { if ( value.length >= 2 && value.charAt(0) === "'" && value.charAt(value.length - 1) === "'" ) { return '"' + value.substr(1, value.length - 2) + '"'; } } return value; } export function smartParse(value: any) { switch (value) { case 'Infinity': return Infinity; case 'NaN': return NaN; case 'undefined': return undefined; default: return JSON5.parse(sanitizeForParse(value)); } } export function smartStringify(value: any) { if (typeof value === 'number') { if (Number.isNaN(value)) { return 'NaN'; } else if (!Number.isFinite(value)) { return 'Infinity'; } } else if (value === undefined) { return 'undefined'; } return JSON.stringify(value); }