Avoid strings if possible for inline snapshots
MODIFY
packages/@romejs/core/test-worker/SnapshotManager.ts
packages/@romejs/core/test-worker/SnapshotManager.ts
@@ -17,6 +17,7 @@ import {DiagnosticDescription, descriptions} from '@romejs/diagnostics';
import createSnapshotParser from './SnapshotParser';
import {ErrorFrame} from '@romejs/v8';
import {Number0, Number1} from '@romejs/ob1';
+import prettyFormat from '@romejs/pretty-format';
function cleanHeading(key: string): string {
if (key[0] === '`') {
@@ -53,7 +54,7 @@ function buildEntriesKey(testName: string, entryName: string): string {
export type InlineSnapshotUpdate = {
line: Number1;
column: Number0;
- snapshot: string;
+ snapshot: boolean | number | string | null | undefined;
};
export type InlineSnapshotUpdates = Array<InlineSnapshotUpdate>;
@@ -306,15 +307,26 @@ export default class SnapshotManager {
testInlineSnapshot(
callFrame: ErrorFrame,
- received: string,
- snapshot?: string,
- ): 'MATCH' | 'NO_MATCH' | 'UPDATE' {
+ received: unknown,
+ expected?: InlineSnapshotUpdate['snapshot'],
+ ): {
+ status: 'MATCH' | 'NO_MATCH' | 'UPDATE';
+ } {
+ let receivedFormat = prettyFormat(received);
+
+ let expectedFormat;
+ if (typeof expected === 'string') {
+ expectedFormat = expected;
+ } else {
+ expectedFormat = prettyFormat(expected);
+ }
+
// Matches, no need to do anything
- if (received === snapshot) {
- return 'MATCH';
+ if (receivedFormat === expectedFormat) {
+ return {status: 'MATCH'};
}
- const shouldSave = this.options.updateSnapshots || snapshot === undefined;
+ const shouldSave = this.options.updateSnapshots || expected === undefined;
if (shouldSave) {
const {lineNumber, columnNumber} = callFrame;
if (lineNumber === undefined || columnNumber === undefined) {
@@ -322,17 +334,28 @@ export default class SnapshotManager {
}
if (!this.options.freezeSnapshots) {
+ let snapshot: InlineSnapshotUpdate['snapshot'] = receivedFormat;
+ if (
+ typeof received === 'string' ||
+ typeof received === 'number' ||
+ typeof received === 'boolean' ||
+ received === undefined ||
+ received === null
+ ) {
+ snapshot = received;
+ }
+
this.inlineSnapshotsUpdates.push({
line: lineNumber,
column: columnNumber,
- snapshot: received,
+ snapshot,
});
}
- return 'UPDATE';
+ return {status: 'UPDATE'};
}
- return 'NO_MATCH';
+ return {status: 'NO_MATCH'};
}
async get(

MODIFY
packages/@romejs/core/test-worker/TestAPI.ts
packages/@romejs/core/test-worker/TestAPI.ts
@@ -575,22 +575,14 @@ export default class TestAPI implements TestHelper {
}
}
- inlineSnapshot(received: unknown, snapshot?: string) {
+ inlineSnapshot(received: unknown, snapshot?: string | boolean | number) {
const callFrame = getErrorStructure(new Error()).frames[1];
-
- let formatted = '';
- if (typeof received === 'string') {
- formatted = received;
- } else {
- formatted = prettyFormat(received);
- }
-
const callError = getErrorStructure(new Error(), 1);
this.onTeardown(async () => {
- const status = this.snapshotManager.testInlineSnapshot(
+ const {status} = this.snapshotManager.testInlineSnapshot(
callFrame,
- formatted,
+ received,
snapshot,
);
@@ -609,7 +601,7 @@ export default class TestAPI implements TestHelper {
...callError,
message: 'Inline snapshots do not match',
advice: this.buildMatchAdvice(
- formatted,
+ received,
snapshot,
{
receivedAlias: 'What the code gave us',

MODIFY
packages/@romejs/core/test-worker/TestWorkerRunner.ts
packages/@romejs/core/test-worker/TestWorkerRunner.ts
@@ -110,7 +110,6 @@ export default class TestWorkerRunner {
);
this.hasDiagnostics = false;
- this.emitConsoleDiagnostics = false;
this.consoleAdvice = [];
this.hasFocusedTest = false;
this.foundTests = new Map();
@@ -133,7 +132,6 @@ export default class TestWorkerRunner {
opts: TestWorkerBridgeRunOptions;
locked: boolean;
consoleAdvice: DiagnosticAdvice;
- emitConsoleDiagnostics: boolean;
hasDiagnostics: boolean;
createConsole(): Partial<Console> {
@@ -237,6 +235,7 @@ export default class TestWorkerRunner {
origin.message = markup`Generated from the file <filelink target="${this.file.uid}" /> and test name "${ref.testName}"`;
}
+ this.hasDiagnostics = true;
await this.bridge.testDiagnostic.call({diagnostic, origin});
}
@@ -244,6 +243,7 @@ export default class TestWorkerRunner {
async discoverTests() {
const {code} = this.opts;
+ try {
const res = await executeMain({
path: this.file.real,
code,
@@ -266,6 +266,22 @@ export default class TestWorkerRunner {
},
});
}
+ } catch (err) {
+ await this.onError(
+ undefined,
+ {
+ error: err,
+ firstAdvice: [],
+ lastAdvice: [
+ {
+ type: 'log',
+ category: 'info',
+ text: markup`Error occured while executing test file <filelink emphasis target="${this.file.uid}" />`,
+ },
+ ],
+ },
+ );
+ }
}
lockTests() {
@@ -324,6 +340,7 @@ export default class TestWorkerRunner {
diagnostic = {
...diagnostic,
+ unique: true,
description: {
...diagnostic.description,
advice: [
@@ -334,7 +351,6 @@ export default class TestWorkerRunner {
},
};
- this.emitConsoleDiagnostics = true;
await this.emitDiagnostic(
diagnostic,
testName === undefined ? undefined : this.createTestRef(testName),
@@ -435,7 +451,10 @@ export default class TestWorkerRunner {
const promises: Set<Promise<void>> = new Set();
const {foundTests} = this;
- if (foundTests.size === 0) {
+
+ // Emit error about no found tests. If we already have diagnostics then there was an issue
+ // during initialization.
+ if (foundTests.size === 0 && !this.hasDiagnostics) {
this.emitDiagnostic({
location: {
filename: this.file.uid,
@@ -481,7 +500,7 @@ export default class TestWorkerRunner {
// Save the snapshot
await this.snapshotManager.save();
- if (this.emitConsoleDiagnostics && this.consoleAdvice.length > 0) {
+ if (this.hasDiagnostics && this.consoleAdvice.length > 0) {
await this.emitDiagnostic({
description: descriptions.TESTS.LOGS(this.consoleAdvice),
location: {

ADD
packages/@romejs/core/test-worker/utils.ts
+/**
+ * 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.
+ */
+
+import prettyFormat from '@romejs/pretty-format';
+
+export function format(value: unknown): string {
+ if (typeof value === 'string') {
+ return value;
+ } else {
+ return prettyFormat(value);
+ }
+}

MODIFY
packages/@romejs/core/worker/WorkerAPI.ts
packages/@romejs/core/worker/WorkerAPI.ts
*/
import {FileReference, Worker} from '@romejs/core';
-import {AnyNode, Program, stringLiteral} from '@romejs/js-ast';
+import {AnyNode, Program} from '@romejs/js-ast';
import {Diagnostics, catchDiagnostics, descriptions} from '@romejs/diagnostics';
import {
CompileResult,
@@ -38,7 +38,7 @@ import {
InlineSnapshotUpdates,
} from '../test-worker/SnapshotManager';
import {formatJS} from '@romejs/js-formatter';
-import {getNodeReferenceParts} from '@romejs/js-ast-utils';
+import {getNodeReferenceParts, valueToNode} from '@romejs/js-ast-utils';
// Some Windows git repos will automatically convert Unix line endings to Windows
// This retains the line endings for the formatted code if they were present in the source
@@ -177,10 +177,7 @@ export default class WorkerAPI {
return {
...node,
- arguments: [
- args[0],
- stringLiteral.create({value: matchedUpdate.snapshot}),
- ],
+ arguments: [args[0], valueToNode(matchedUpdate.snapshot)],
};
}

MODIFY
packages/@romejs/diagnostics/DiagnosticsProcessor.ts
packages/@romejs/diagnostics/DiagnosticsProcessor.ts
@@ -210,6 +210,10 @@ export default class DiagnosticsProcessor {
}
buildDedupeKeys(diag: Diagnostic): Array<string> {
+ if (diag.unique) {
+ return [];
+ }
+
// We don't do anything with `end` in this method, it's fairly meaningless for deduping errors
let {start} = diag.location;

MODIFY
packages/@romejs/diagnostics/types.ts
packages/@romejs/diagnostics/types.ts
@@ -63,6 +63,7 @@ export type DiagnosticSourceType = 'unknown' | ConstSourceType;
export type Diagnostic = {
description: DiagnosticDescription;
location: DiagnosticLocation;
+ unique?: boolean;
fixable?: boolean;
label?: string;
origins?: Array<DiagnosticOrigin>;

ADD
packages/@romejs/js-ast-utils/createMemberProperty.ts
+/**
+ * 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.
+ */
+
+import {
+ ComputedMemberProperty,
+ StaticMemberProperty,
+ computedMemberProperty,
+ identifier,
+ staticMemberProperty,
+ stringLiteral,
+} from '@romejs/js-ast';
+import isValidIdentifierName from './isValidIdentifierName';
+
+export default function createMemberProperty(
+ name: string,
+): StaticMemberProperty | ComputedMemberProperty {
+ if (isValidIdentifierName(name)) {
+ return staticMemberProperty.quick(identifier.quick(name));
+ } else {
+ return computedMemberProperty.quick(stringLiteral.quick(name));
+ }
+}

ADD
packages/@romejs/js-ast-utils/createPropertyKey.ts
+/**
+ * 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.
+ */
+
+import isValidIdentifierName from './isValidIdentifierName';
+import {
+ Identifier,
+ StringLiteral,
+ identifier,
+ stringLiteral,
+} from '@romejs/js-ast';
+
+export default function createPropertyKey(
+ name: string,
+): Identifier | StringLiteral {
+ if (isValidIdentifierName(name)) {
+ return identifier.quick(name);
+ } else {
+ return stringLiteral.quick(name);
+ }
+}

MODIFY
packages/@romejs/js-ast-utils/doesNodeMatchPattern.test.ts
packages/@romejs/js-ast-utils/doesNodeMatchPattern.test.ts
@@ -14,32 +14,32 @@ test(
(t) => {
t.inlineSnapshot(
doesNodeMatchPattern(template.expression`foo`, 'foo'),
- 'true',
+ true,
);
t.inlineSnapshot(
doesNodeMatchPattern(template.expression`this.foo`, 'this.foo'),
- 'true',
+ true,
);
t.inlineSnapshot(
doesNodeMatchPattern(template.expression`exports.foo`, 'exports.**'),
- 'true',
+ true,
);
t.inlineSnapshot(
doesNodeMatchPattern(template.expression`this.foo.bar`, 'this.foo.*'),
- 'true',
+ true,
);
t.inlineSnapshot(
doesNodeMatchPattern(template.expression`this.foo.bar.yes`, 'this.foo.*'),
- 'false',
+ false,
);
t.inlineSnapshot(
doesNodeMatchPattern(template.expression`this.foo.bar.yes`, 'this.foo.**'),
- 'true',
+ true,
);
},
);

MODIFY
packages/@romejs/js-ast-utils/index.ts
packages/@romejs/js-ast-utils/index.ts
@@ -35,3 +35,6 @@ export {default as removeLoc} from './removeLoc';
export {default as removeShallowLoc} from './removeShallowLoc';
export {default as renameBindings} from './renameBindings';
export {default as template} from './template';
+export {default as valueToNode} from './valueToNode';
+export {default as createPropertyKey} from './createPropertyKey';
+export {default as createMemberProperty} from './createMemberProperty';

ADD
packages/@romejs/js-ast-utils/valueToNode.ts
+/**
+ * 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.
+ */
+
+import {
+ ArrayExpression,
+ BooleanLiteral,
+ NullLiteral,
+ NumericLiteral,
+ ObjectExpression,
+ ObjectProperty,
+ ReferenceIdentifier,
+ StringLiteral,
+ arrayExpression,
+ booleanLiteral,
+ nullLiteral,
+ numericLiteral,
+ objectExpression,
+ objectProperty,
+ referenceIdentifier,
+ staticPropertyKey,
+ stringLiteral,
+} from '@romejs/js-ast';
+import createPropertyKey from './createPropertyKey';
+import {UnknownObject} from '@romejs/typescript-helpers';
+
+export default function valueToNode(
+ value: unknown,
+ ancestry: Array<unknown> = [],
+):
+ | StringLiteral
+ | BooleanLiteral
+ | NumericLiteral
+ | ObjectExpression
+ | NullLiteral
+ | ReferenceIdentifier
+ | ArrayExpression {
+ if (ancestry.includes(value)) {
+ throw new Error('Recursion detected');
+ }
+
+ switch (typeof value) {
+ case 'string':
+ return stringLiteral.quick(value);
+
+ case 'boolean':
+ return booleanLiteral.quick(value);
+
+ case 'number':
+ return numericLiteral.quick(value);
+
+ case 'undefined':
+ return referenceIdentifier.quick('undefined');
+
+ case 'object': {
+ if (value === null) {
+ return nullLiteral.create({});
+ }
+
+ const subAncestry = [...ancestry, value];
+
+ if (Array.isArray(value)) {
+ return arrayExpression.quick(
+ value.map((elem) => valueToNode(elem, subAncestry)),
+ );
+ }
+
+ const obj = (value as UnknownObject);
+ const props: Array<ObjectProperty> = [];
+
+ for (let key in obj) {
+ props.push(
+ objectProperty.create({
+ key: staticPropertyKey.create({
+ value: createPropertyKey(key),
+ }),
+ value: valueToNode(obj[key], subAncestry),
+ }),
+ );
+ }
+
+ return objectExpression.quick(props);
+ }
+
+ default:
+ throw new Error('Do not know how to turn this value into a literal');
+ }
+}

MODIFY
packages/@romejs/js-ast/literals/NumericLiteral.ts
packages/@romejs/js-ast/literals/NumericLiteral.ts
*/
import {JSNodeBase} from '../index';
-import {createBuilder} from '../utils';
+import {createQuickBuilder} from '../utils';
export type NumericLiteral = JSNodeBase & {
type: 'NumericLiteral';
@@ -14,8 +14,9 @@ export type NumericLiteral = JSNodeBase & {
format?: 'octal' | 'binary' | 'hex';
};
-export const numericLiteral = createBuilder<NumericLiteral>(
+export const numericLiteral = createQuickBuilder<NumericLiteral, 'value'>(
'NumericLiteral',
+ 'value',
{
bindingKeys: {},
visitorKeys: {},

MODIFY
packages/@romejs/js-ast/utils.ts
packages/@romejs/js-ast/utils.ts
* LICENSE file in the root directory of this source tree.
*/
+import {assertSingleNode, inheritLoc} from '@romejs/js-ast-utils';
import {NodeBase} from '@romejs/parser-core';
import {AnyNode} from './index';
-import {assertSingleNode, inheritLoc} from '@romejs/js-ast-utils';
import {JSNodeBase} from './base';
import {TransformExitResult} from '@romejs/js-compiler';

Avoid strings if possible for inline snapshots