Update extension to .snap.md, add autogenerated message, fix parser
MODIFY
packages/@romejs/core/common/bridges/TestWorkerBridge.ts
packages/@romejs/core/common/bridges/TestWorkerBridge.ts
@@ -10,6 +10,7 @@ import {SourceMap} from '@romejs/codec-source-map';
import {TestRunnerOptions} from '../../master/testing/types';
import {Bridge} from '@romejs/events';
import {JSONFileReference} from '../types/files';
+import {AbsoluteFilePath} from '@romejs/path';
export type TestRef = {
filename: string;
@@ -18,6 +19,7 @@ export type TestRef = {
export type TestWorkerBridgeRunOptions = {
file: JSONFileReference;
+ projectFolder: string;
code: string;
cwd: string;
sourceMap: SourceMap;

MODIFY
packages/@romejs/core/master/testing/TestRunner.ts
packages/@romejs/core/master/testing/TestRunner.ts
@@ -38,6 +38,7 @@ import {
CoverageFolder,
} from './types';
import {percentInsideCoverageFolder, formatPercent, sortMapKeys} from './utils';
+import {serializeJSONProjectConfig} from '@romejs/project';
export default class TestRunner {
constructor(opts: TestRunnerConstructorOptions) {
@@ -130,6 +131,9 @@ export default class TestRunner {
try {
await bridge.runTest.call({
options: opts,
+ projectFolder: req.master.projectManager
+ .assertProjectExisting(path)
+ .folder.join(),
file: req.master.projectManager.getTransportFileReference(path),
cwd: flags.cwd.join(),
code,

MODIFY
packages/@romejs/core/test-worker/SnapshotManager.ts
packages/@romejs/core/test-worker/SnapshotManager.ts
@@ -17,6 +17,7 @@ import {TestRunnerOptions} from '../master/testing/types';
import TestWorkerRunner from './TestWorkerRunner';
import {PartialDiagnosticAdvice} from '@romejs/diagnostics';
import createSnapshotParser from './SnapshotParser';
+import stringDiff from '@romejs/string-diff';
const SNAPSHOTS_DIR = '__rsnapshots__';
@@ -44,7 +45,7 @@ export default class SnapshotManager {
this.options = runner.options;
this.exists = false;
- this.raw = undefined;
+ this.raw = '';
this.entries = new Map();
}
@@ -53,17 +54,20 @@ export default class SnapshotManager {
folder: AbsoluteFilePath;
path: AbsoluteFilePath;
entries: Map<
+ string,
+ Map<
string,
{
language: undefined | string;
value: string;
}
+ >
>;
runner: TestWorkerRunner;
options: TestRunnerOptions;
- raw: undefined | string;
+ raw: string;
exists: boolean;
async emitDiagnostic(message: string, advice?: PartialDiagnosticAdvice) {
@@ -110,6 +114,18 @@ export default class SnapshotManager {
}
if (node.type === 'Heading' && node.level === 2) {
+ const testName = cleanHeading(node.text);
+ const entries = new Map();
+ this.entries.set(testName, entries);
+
+ while (nodes.length > 0) {
+ const node = nodes[0];
+
+ if (node.type === 'Heading' && node.level === 3) {
+ nodes.shift();
+
+ const snapshotName = cleanHeading(node.text);
+
const codeBlock = nodes.shift();
if (codeBlock === undefined || codeBlock.type !== 'CodeBlock') {
throw parser.unexpected({
@@ -118,18 +134,28 @@ export default class SnapshotManager {
});
}
- this.entries.set(cleanHeading(node.text), {
+ entries.set(snapshotName, {
language: codeBlock.language,
value: codeBlock.text,
});
continue;
}
- throw parser.unexpected({
- message: 'Unexpected node',
- loc: node.loc,
+ if (node.type === 'CodeBlock') {
+ nodes.shift();
+
+ entries.set('0', {
+ language: node.language,
+ value: node.text,
});
}
+
+ break;
+ }
+
+ continue;
+ }
+ }
}
async save() {
@@ -152,14 +178,39 @@ export default class SnapshotManager {
// Build the snapshot
let lines: Array<string> = [];
- lines.push(`# \`${this.testPath.getBasename()}\``);
+
+ function pushNewline() {
+ if (lines[lines.length - 1] !== '') {
lines.push('');
+ }
+ }
- // Get keys and sort them so they're in a predictable order
- const keys = Array.from(this.entries.keys()).sort();
+ lines.push(`# \`${this.testPath.getBasename()}\``);
+ pushNewline();
+ const relativeTestPath = this.runner.projectFolder
+ .relative(this.testPath)
+ .join();
+ lines.push(
+ `DO NOT MODIFY. This file has been autogenerated. Run \`rome test ${relativeTestPath} --update-snapshots\` to update.`,
+ );
+ pushNewline();
+
+ // Get test names and sort them so they are in a predictable
+ const testNames = Array.from(this.entries.keys()).sort();
+
+ for (const testName of testNames) {
+ const entries = this.entries.get(testName);
+ if (entries === undefined) {
+ throw new Error('Impossible');
+ }
+
+ lines.push(`## \`${testName}\``);
+ pushNewline();
- for (const key of keys) {
- const entry = this.entries.get(key);
+ const snapshotNames = Array.from(entries.keys()).sort();
+
+ for (const snapshotName of snapshotNames) {
+ const entry = entries.get(snapshotName);
if (entry === undefined) {
throw new Error('Impossible');
}
@@ -167,13 +218,19 @@ export default class SnapshotManager {
const {value} = entry;
const language = entry.language === undefined ? '' : entry.language;
- lines.push(`## \`${key}\``);
- lines.push('');
+ // If the test only has one snapshot then omit the heading
+ const skipHeading = snapshotName === '0' && snapshotNames.length === 1;
+ if (!skipHeading) {
+ lines.push(`### \`${snapshotName}\``);
+ }
+
+ pushNewline();
lines.push('```' + language);
// TODO escape triple backquotes
lines.push(value);
lines.push('```');
- lines.push('');
+ pushNewline();
+ }
}
const formatted = lines.join('\n');
@@ -182,7 +239,12 @@ export default class SnapshotManager {
if (!this.exists) {
await this.emitDiagnostic('Snapshot does not exist');
} else if (formatted !== this.raw) {
- await this.emitDiagnostic('Snapshots do not match');
+ await this.emitDiagnostic('Snapshots do not match', [
+ {
+ type: 'diff',
+ diff: stringDiff(this.raw, formatted),
+ },
+ ]);
}
} else if (formatted !== this.raw) {
// Create snapshots directory if it doesn't exist
@@ -195,22 +257,33 @@ export default class SnapshotManager {
}
}
- toSnapshotKey(testName: string, id: number | string): string {
- if (id === 0) {
- return testName;
- } else {
- return `${testName}: ${id}`;
- }
- }
-
- get(key: string): undefined | string {
- const entry = this.entries.get(key);
+ get(testName: string, snapshotName: string): undefined | string {
+ const entries = this.entries.get(testName);
+ if (entries !== undefined) {
+ const entry = entries.get(snapshotName);
if (entry !== undefined) {
return entry.value;
}
}
+ }
+
+ set({
+ value,
+ language,
+ testName,
+ snapshotName,
+ }: {
+ value: string;
+ language: undefined | string;
+ testName: string;
+ snapshotName: string;
+ }) {
+ let entries = this.entries.get(testName);
+ if (entries === undefined) {
+ entries = new Map();
+ this.entries.set(testName, entries);
+ }
- set(key: string, value: string, language: undefined | string) {
- this.entries.set(key, {value, language});
+ entries.set(snapshotName, {value, language});
}
}

MODIFY
packages/@romejs/core/test-worker/SnapshotParser.ts
packages/@romejs/core/test-worker/SnapshotParser.ts
@@ -36,7 +36,12 @@ type CodeBlockNode = NodeBase & {
text: string;
};
-type Node = HeadingNode | CodeBlockNode;
+type TextLineNode = NodeBase & {
+ type: 'TextLine';
+ text: string;
+};
+
+type Node = HeadingNode | CodeBlockNode | TextLineNode;
function isHash(char: string): boolean {
return char === '#';
@@ -68,6 +73,7 @@ export default createParser(
class SnapshotParser extends ParserCore<Tokens, void> {
constructor(opts: ParserOptions) {
super(opts, 'snapshots');
+ this.ignoreWhitespaceTokens = true;
}
tokenize(index: Number0, input: string) {
@@ -79,12 +85,6 @@ export default createParser(
const level = hashes.length;
return this.finishValueToken('Hashes', level, add(index, level));
- case ' ':
- case '\t':
- case '\r':
- case '\n':
- return this.lookaheadToken(inc(index));
-
case '`':
const nextChar = input[get0(add(index, 1))];
const nextNextChar = input[get0(add(index, 2))];
@@ -147,8 +147,8 @@ export default createParser(
}
}
- const [text] = this.readInputFrom(index, isntNewline);
- return this.finishValueToken('TextLine', text, add(index, text.length));
+ const [text, end] = this.readInputFrom(index, isntNewline);
+ return this.finishValueToken('TextLine', text, end);
}
parse(): Array<Node> {
@@ -180,6 +180,15 @@ export default createParser(
this.nextToken();
break;
+ case 'TextLine':
+ nodes.push({
+ type: 'TextLine',
+ text: token.value,
+ loc: this.finishLoc(start),
+ });
+ this.nextToken();
+ break;
+
default:
throw this.unexpected();
}

MODIFY
packages/@romejs/core/test-worker/TestAPI.ts
packages/@romejs/core/test-worker/TestAPI.ts
@@ -509,26 +509,23 @@ export default class TestAPI {
snapshot(expected: unknown, message?: string) {
const id = this.snapshotCounter++;
- return this._snapshotNamed(id, expected, message, 2);
+ return this._snapshotNamed(String(id), expected, message, 2);
}
- snapshotNamed(name: string | number, expected: unknown, message?: string) {
+ snapshotNamed(name: string, expected: unknown, message?: string) {
return this._snapshotNamed(name, expected, message, 1);
}
- getSnapshot(name: string | number): unknown {
- const key = this.snapshotManager.toSnapshotKey(this.testName, name);
- return this.snapshotManager.get(key);
+ getSnapshot(snapshotName: string): unknown {
+ return this.snapshotManager.get(this.testName, snapshotName);
}
_snapshotNamed(
- name: string | number,
+ name: string,
expected: unknown,
message: string = "Snapshots don't match",
framesToPop?: number,
) {
- const key = this.snapshotManager.toSnapshotKey(this.testName, name);
-
let language: undefined | string;
let formatted = '';
@@ -540,10 +537,15 @@ export default class TestAPI {
}
// Get the current snapshot
- const existingSnapshot = this.snapshotManager.get(key);
+ const existingSnapshot = this.snapshotManager.get(this.testName, name);
if (existingSnapshot === undefined) {
// No snapshot exists, let's save this one!
- this.snapshotManager.set(key, formatted, language);
+ this.snapshotManager.set({
+ testName: this.testName,
+ snapshotName: String(name),
+ value: formatted,
+ language,
+ });
return undefined;
}

MODIFY
packages/@romejs/core/test-worker/TestWorkerRunner.ts
packages/@romejs/core/test-worker/TestWorkerRunner.ts
@@ -28,7 +28,7 @@ import {
convertTransportFileReference,
} from '../common/types/files';
import {GlobalTestOptions} from '@romejs/test';
-import {createAbsoluteFilePath} from '@romejs/path';
+import {createAbsoluteFilePath, AbsoluteFilePath} from '@romejs/path';
const MAX_RUNNING_TESTS = 20;
@@ -39,6 +39,7 @@ export default class TestWorkerRunner {
this.file = convertTransportFileReference(opts.file);
this.options = opts.options;
this.bridge = bridge;
+ this.projectFolder = createAbsoluteFilePath(opts.projectFolder);
this.snapshotManager = new SnapshotManager(
this,
@@ -60,6 +61,7 @@ export default class TestWorkerRunner {
hasFocusedTest: boolean;
bridge: TestWorkerBridge;
+ projectFolder: AbsoluteFilePath;
file: FileReference;
options: TestRunnerOptions;
snapshotManager: SnapshotManager;

Update extension to .snap.md, add autogenerated message, fix parser