Add inline snapshot support
MODIFY
packages/@romejs-runtime/rome/test.ts
packages/@romejs-runtime/rome/test.ts
@@ -85,6 +85,7 @@ export interface TestHelper {
message?: string,
opts?: TestSnapshotOptions,
): Promise<string>;
+ inlineSnapshot(received: unknown, expected?: string): void;
snapshotNamed(
name: string,
expected: unknown,

MODIFY
packages/@romejs/cli-diagnostics/printAdvice.ts
packages/@romejs/cli-diagnostics/printAdvice.ts
@@ -450,6 +450,7 @@ function printStacktrace(
},
{
ordered: true,
+ reverse: true,
truncate: opts.flags.verboseDiagnostics ? undefined : 20,
},
);

MODIFY
packages/@romejs/core/common/bridges/TestWorkerBridge.ts
packages/@romejs/core/common/bridges/TestWorkerBridge.ts
@@ -9,6 +9,7 @@ import {Diagnostic} from '@romejs/diagnostics';
import {TestRunnerOptions} from '../../master/testing/types';
import {Bridge} from '@romejs/events';
import {JSONFileReference} from '../types/files';
+import {TestWorkerFileResult} from '@romejs/core/test-worker/TestWorkerRunner';
export type TestRef = {
filename: string;
@@ -40,18 +41,7 @@ export default class TestWorkerBridge extends Bridge {
direction: 'server->client',
});
- snapshotUpdated = this.createEvent<
- {
- filename: string;
- event: 'delete' | 'update' | 'create';
- },
- void
- >({
- name: 'snapshotUpdated',
- direction: 'server<-client',
- });
-
- runTest = this.createEvent<number, void>({
+ runTest = this.createEvent<number, TestWorkerFileResult>({
name: 'runTest',
direction: 'server->client',
});

MODIFY
packages/@romejs/core/master/testing/TestRunner.ts
packages/@romejs/core/master/testing/TestRunner.ts
@@ -49,6 +49,7 @@ import {escapeMarkup, markup} from '@romejs/string-markup';
import {MAX_WORKER_COUNT} from '@romejs/core/common/constants';
import {TestWorkerFlags} from '@romejs/core/test-worker/TestWorker';
import net = require('net');
+import {TestWorkerFileResult} from '@romejs/core/test-worker/TestWorkerRunner';
class BridgeStructuredError extends NativeStructuredError {
constructor(struct: Partial<StructuredError>, bridge: Bridge) {
@@ -164,6 +165,18 @@ export default class TestRunner {
createdSnapshots: number;
};
+ async processTestResult(
+ path: AbsoluteFilePath,
+ res: TestWorkerFileResult,
+ ): Promise<void> {
+ this.progress.createdSnapshots += res.snapshotCounts.created;
+ this.progress.updatedSnapshots += res.snapshotCounts.updated;
+ this.progress.deletedSnapshots += res.snapshotCounts.deleted;
+
+ // TODO
+ res.inlineSnapshotUpdates;
+ }
+
async runWorker({bridge, process, inspector}: TestWorkerContainer) {
const {options: opts, sourcesQueue} = this;
const req = this.request;
@@ -213,7 +226,8 @@ export default class TestRunner {
code,
});
- await bridge.runTest.call(id);
+ const result = await bridge.runTest.call(id);
+ await this.processTestResult(path, result);
} finally {
removeSourceMap();
}
@@ -591,25 +605,6 @@ export default class TestRunner {
}
});
- bridge.snapshotUpdated.subscribe(({event}) => {
- switch (event) {
- case 'create': {
- this.progress.createdSnapshots++;
- break;
- }
-
- case 'update': {
- this.progress.updatedSnapshots++;
- break;
- }
-
- case 'delete': {
- this.progress.deletedSnapshots++;
- break;
- }
- }
- });
-
bridge.testsFound.subscribe((tests) => {
for (const {ref, isSkipped} of tests) {
this.onTestFound(ref, isSkipped);

MODIFY
packages/@romejs/core/test-worker/SnapshotManager.ts
packages/@romejs/core/test-worker/SnapshotManager.ts
@@ -15,6 +15,8 @@ import {TestRunnerOptions} from '../master/testing/types';
import TestWorkerRunner from './TestWorkerRunner';
import {DiagnosticDescription, descriptions} from '@romejs/diagnostics';
import createSnapshotParser from './SnapshotParser';
+import {ErrorFrame} from '@romejs/v8';
+import {Number0, Number1} from '@romejs/ob1';
function cleanHeading(key: string): string {
if (key[0] === '`') {
@@ -48,6 +50,18 @@ function buildEntriesKey(testName: string, entryName: string): string {
return `${testName}#${entryName}`;
}
+export type InlineSnapshotUpdate = {
+ line: Number1;
+ column: Number0;
+ snapshot: string;
+};
+
+export type SnapshotCounts = {
+ deleted: number;
+ updated: number;
+ created: number;
+};
+
export default class SnapshotManager {
constructor(runner: TestWorkerRunner, testPath: AbsoluteFilePath) {
this.defaultSnapshotPath = testPath.getParent().append(
@@ -58,21 +72,21 @@ export default class SnapshotManager {
this.runner = runner;
this.options = runner.options;
this.snapshots = new AbsoluteFilePathMap();
- this.loadingSnapshotCount = 0;
+ this.inlineSnapshotsUpdates = [];
+ this.snapshotCounts = {
+ deleted: 0,
+ updated: 0,
+ created: 0,
+ };
}
- loadingSnapshotCount: number;
+ inlineSnapshotsUpdates: Array<InlineSnapshotUpdate>;
testPath: AbsoluteFilePath;
defaultSnapshotPath: AbsoluteFilePath;
snapshots: AbsoluteFilePathMap<Snapshot>;
runner: TestWorkerRunner;
options: TestRunnerOptions;
-
- teardown() {
- if (this.loadingSnapshotCount > 0) {
- throw new Error();
- }
- }
+ snapshotCounts: SnapshotCounts;
normalizeSnapshotPath(filename: undefined | string): AbsoluteFilePath {
if (filename === undefined) {
@@ -260,8 +274,6 @@ export default class SnapshotManager {
const lines = this.buildSnapshot(entries.values());
const formatted = lines.join('\n');
- let event: undefined | 'delete' | 'update' | 'create';
-
if (this.options.freezeSnapshots) {
if (!used) {
await this.emitDiagnostic(descriptions.SNAPSHOTS.REDUNDANT);
@@ -278,22 +290,56 @@ export default class SnapshotManager {
if (!hasDiagnostics) {
// If a snapshot wasn't used or is empty then delete it!
await unlink(path);
- event = 'delete';
+ this.snapshotCounts.deleted++;
}
} else if (used && formatted !== raw) {
// Fresh snapshot!
await writeFile(path, formatted);
- event = existsOnDisk ? 'update' : 'create';
+ if (existsOnDisk) {
+ this.snapshotCounts.updated++;
+ } else {
+ this.snapshotCounts.created++;
+ }
+ }
+ }
}
}
- if (event !== undefined) {
- await this.runner.bridge.snapshotUpdated.call({
- filename: path.join(),
- event,
- });
+ testInlineSnapshot(
+ callFrame: ErrorFrame,
+ received: string,
+ snapshot?: string,
+ ): boolean {
+ // Matches, no need to do anything
+ if (received === snapshot) {
+ return true;
}
+
+ const shouldSave = this.options.updateSnapshots || snapshot === undefined;
+
+ if (snapshot === undefined) {
+ snapshot = received;
}
+
+ if (shouldSave) {
+ const {lineNumber, columnNumber} = callFrame;
+ if (lineNumber === undefined || columnNumber === undefined) {
+ throw new Error('Call frame has no line or column');
+ }
+
+ if (this.options.freezeSnapshots) {
+ throw new Error('Requested a snapshot update but frozen');
+ }
+
+ this.inlineSnapshotsUpdates.push({
+ line: lineNumber,
+ column: columnNumber,
+ snapshot,
+ });
+ return true;
+ }
+
+ return false;
}
async get(

MODIFY
packages/@romejs/core/test-worker/TestAPI.ts
packages/@romejs/core/test-worker/TestAPI.ts
@@ -10,7 +10,7 @@ import SnapshotManager from './SnapshotManager';
import {TestRunnerOptions} from '../master/testing/types';
import {Event} from '@romejs/events';
import diff from '@romejs/string-diff';
-import {createErrorFromStructure} from '@romejs/v8';
+import {createErrorFromStructure, getErrorStructure} from '@romejs/v8';
import prettyFormat from '@romejs/pretty-format';
import {FileReference} from '../common/types/files';
import {markup} from '@romejs/string-markup';
@@ -272,14 +272,17 @@ export default class TestAPI implements TestHelper {
fail(
message: string = 'Test failure triggered by t.fail()',
advice: DiagnosticAdvice = [],
- framesToPop: number = 0,
+ framesToShift: number = 0,
): never {
- throw createErrorFromStructure({
+ throw createErrorFromStructure(
+ {
+ ...getErrorStructure(new Error()),
message,
markupMessage: message,
advice,
- framesToPop: framesToPop + 1,
- });
+ },
+ framesToShift + 1,
+ );
}
truthy(value: unknown, message: string = 'Expected value to be truthy'): void {
@@ -575,6 +578,37 @@ export default class TestAPI implements TestHelper {
}
}
+ inlineSnapshot(received: unknown, snapshot?: string) {
+ const callFrame = getErrorStructure(new Error()).frames[1];
+
+ let formatted = '';
+ if (typeof received === 'string') {
+ formatted = received;
+ } else {
+ formatted = prettyFormat(received);
+ }
+
+ const same = this.snapshotManager.testInlineSnapshot(
+ callFrame,
+ formatted,
+ snapshot,
+ );
+ if (!same) {
+ this.fail(
+ 'Inline snapshots do not match',
+ this.buildMatchAdvice(
+ formatted,
+ snapshot,
+ {
+ receivedAlias: 'What the code gave us',
+ expectedAlias: 'Existing inline snapshot',
+ },
+ ),
+ 1,
+ );
+ }
+ }
+
snapshot(
expected: unknown,
message?: string,

MODIFY
packages/@romejs/core/test-worker/TestWorker.ts
packages/@romejs/core/test-worker/TestWorker.ts
@@ -9,7 +9,7 @@ import {TestWorkerBridgeRunOptions} from '../common/bridges/TestWorkerBridge';
import {deriveDiagnosticFromError} from '@romejs/diagnostics';
import {TestWorkerBridge} from '@romejs/core';
import {createBridgeFromParentProcess} from '@romejs/events';
-import TestWorkerRunner from './TestWorkerRunner';
+import TestWorkerRunner, {TestWorkerFileResult} from './TestWorkerRunner';
import inspector = require('inspector');
export type TestWorkerFlags = {
@@ -69,12 +69,12 @@ export default class TestWorker {
return bridge;
}
- async runTest(id: number): Promise<void> {
+ async runTest(id: number): Promise<TestWorkerFileResult> {
const runner = this.runners.get(id);
if (runner === undefined) {
throw new Error(`No runner ${id} found`);
} else {
- await runner.run();
+ return await runner.run();
}
}

MODIFY
packages/@romejs/core/test-worker/TestWorkerRunner.ts
packages/@romejs/core/test-worker/TestWorkerRunner.ts
@@ -29,7 +29,10 @@ import {
TestWorkerBridgeRunOptions,
} from '../common/bridges/TestWorkerBridge';
import {TestRunnerOptions} from '../master/testing/types';
-import SnapshotManager from './SnapshotManager';
+import SnapshotManager, {
+ InlineSnapshotUpdate,
+ SnapshotCounts,
+} from './SnapshotManager';
import TestAPI, {OnTimeout} from './TestAPI';
import executeMain from '../common/utils/executeMain';
import {
@@ -86,6 +89,11 @@ function cleanFrames(frames: ErrorFrames): ErrorFrames {
return frames.slice(0, frames.indexOf(latestTestWorkerFrame));
}
+export type TestWorkerFileResult = {
+ snapshotCounts: SnapshotCounts;
+ inlineSnapshotUpdates: Array<InlineSnapshotUpdate>;
+};
+
export default class TestWorkerRunner {
constructor(opts: TestWorkerBridgeRunOptions, bridge: TestWorkerBridge) {
this.opts = opts;
@@ -404,7 +412,7 @@ export default class TestWorkerRunner {
}
}
- async run(): Promise<void> {
+ async run(): Promise<TestWorkerFileResult> {
const promises: Set<Promise<void>> = new Set();
const {foundTests} = this;
@@ -462,6 +470,11 @@ export default class TestWorkerRunner {
},
});
}
+
+ return {
+ inlineSnapshotUpdates: this.snapshotManager.inlineSnapshotsUpdates,
+ snapshotCounts: this.snapshotManager.snapshotCounts,
+ };
}
async emitFoundTests() {

MODIFY
packages/@romejs/v8/errors.ts
packages/@romejs/v8/errors.ts
@@ -25,11 +25,10 @@ export type StructuredError = {
stack: undefined | string;
frames: ErrorFrames;
advice: DiagnosticAdvice;
- framesToPop: number;
};
export class NativeStructuredError extends Error {
- constructor(struct: Partial<StructuredError>) {
+ constructor(struct: Partial<StructuredError>, framesToShift?: number) {
super(struct.message);
this.name = struct.name === undefined ? 'Error' : struct.name;
this.stack = struct.stack;
@@ -37,7 +36,7 @@ export class NativeStructuredError extends Error {
this[ERROR_MARKUP_MESSAGE_PROP] = struct.markupMessage;
this[ERROR_FRAMES_PROP] = struct.frames;
this[ERROR_ADVICE_PROP] = struct.advice;
- this[ERROR_POP_FRAMES_PROP] = struct.framesToPop;
+ this[ERROR_POP_FRAMES_PROP] = framesToShift;
}
[ERROR_MARKUP_MESSAGE_PROP]: undefined | string;
@@ -48,8 +47,9 @@ export class NativeStructuredError extends Error {
export function createErrorFromStructure(
struct: Partial<StructuredError>,
+ framesToShift: undefined | number,
): Error {
- return new NativeStructuredError(struct);
+ return new NativeStructuredError(struct, framesToShift);
}
export function getErrorStructure(err: unknown): StructuredError {
@@ -59,7 +59,6 @@ export function getErrorStructure(err: unknown): StructuredError {
let markupMessage: string | undefined = undefined;
let frames: ErrorFrames = [];
let advice: DiagnosticAdvice = [];
- let framesToPop = 0;
let looksLikeValidError = false;
if (
@@ -100,9 +99,9 @@ export function getErrorStructure(err: unknown): StructuredError {
advice = err[ERROR_ADVICE_PROP];
}
- const _framesToPop = err[ERROR_POP_FRAMES_PROP];
- if (typeof _framesToPop === 'number') {
- framesToPop = _framesToPop;
+ const framesToShift = err[ERROR_POP_FRAMES_PROP];
+ if (typeof framesToShift === 'number') {
+ frames = frames.slice(framesToShift);
}
}
@@ -117,7 +116,6 @@ export function getErrorStructure(err: unknown): StructuredError {
stack,
frames,
advice,
- framesToPop,
};
}

Add inline snapshot support