Skip to content

Commit 2cdb2a9

Browse files
committed
chore: add named snapshot support
1 parent 14b6369 commit 2cdb2a9

File tree

3 files changed

+152
-33
lines changed

3 files changed

+152
-33
lines changed

packages/alphatab/test/PrettyFormat.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,48 @@ export class PrettyFormat {
210210
: `Map {${PrettyFormat.printIteratorEntries(TestPlatform.mapAsUnknownIterable(val), config, indentation, depth, refs, PrettyFormat.printer, ' => ')}}`;
211211
}
212212

213-
return '';
213+
// Avoid failure to serialize global window object in jsdom test environment.
214+
// For example, not even relevant if window is prop of React element.
215+
const constructorName = TestPlatform.getConstructorName(val);
216+
return hitMaxDepth
217+
? `[${constructorName}]`
218+
: `${
219+
min ? '' : !config.printBasicPrototype && constructorName === 'Object' ? '' : `${constructorName} `
220+
}{${PrettyFormat._printObjectProperties(val as object, config, indentation, depth, refs)}}`;
221+
}
222+
223+
private static _printObjectProperties(
224+
val: object,
225+
config: PrettyFormatConfig,
226+
indentation: string,
227+
depth: number,
228+
refs: unknown[]
229+
) {
230+
let result = '';
231+
const entries = Object.entries(val);
232+
233+
if (entries.length > 0) {
234+
result += config.spacingOuter;
235+
236+
const indentationNext = indentation + config.indent;
237+
238+
for (let i = 0; i < entries.length; i++) {
239+
const name = PrettyFormat.printer(entries[i][0], config, indentationNext, depth, refs);
240+
const value = PrettyFormat.printer(entries[i][1], config, indentationNext, depth, refs);
241+
242+
result += `${indentationNext + name}: ${value}`;
243+
244+
if (i < entries.length - 1) {
245+
result += `,${config.spacingInner}`;
246+
} else if (!config.min) {
247+
result += ',';
248+
}
249+
}
250+
251+
result += config.spacingOuter + indentation;
252+
}
253+
254+
return result;
214255
}
215256

216257
/**
@@ -599,7 +640,10 @@ export class SnapshotFile {
599640
return `No snapshot '${name}' found`;
600641
}
601642

602-
const actual = PrettyFormat.format(value, SnapshotFile.matchOptions).split('\n');
643+
// https://github.com/jestjs/jest/blob/8e683abe2a1d3f6f6513dd9467f0f49d3d2ffc0d/packages/jest-snapshot-utils/src/utils.ts#L190C51-L190C70
644+
const actual = SnapshotFile._printBacktickString(PrettyFormat.format(value, SnapshotFile._matchOptions)).split(
645+
'\n'
646+
);
603647

604648
const lines = Math.min(expected.length, actual.length);
605649
const errors: string[] = [];
@@ -622,6 +666,15 @@ export class SnapshotFile {
622666
return null;
623667
}
624668

669+
// https://github.com/jestjs/jest/blob/8e683abe2a1d3f6f6513dd9467f0f49d3d2ffc0d/packages/jest-snapshot-utils/src/utils.ts#L167-L171
670+
private static _printBacktickString(str: string) {
671+
return SnapshotFile._escapeBacktickString(str);
672+
}
673+
674+
private static _escapeBacktickString(str: string) {
675+
return str.replaceAll(/[`\\]/g, (substring: string) => `\\${substring}`);
676+
}
677+
625678
loadFrom(path: string) {
626679
const content = TestPlatform.loadFileAsStringSync(path);
627680

@@ -638,6 +691,13 @@ export class SnapshotFile {
638691
}
639692

640693
const name = lines[i].substring(9, endOfName);
694+
695+
if (lines[i].endsWith('`;')) {
696+
const startOfValue = lines[i].indexOf('`', endOfName + 2) + 1;
697+
this.snapshots.set(name, lines[i].substring(startOfValue, lines[i].length - 2));
698+
i++;
699+
continue;
700+
}
641701
i++;
642702

643703
let value = '';

packages/csharp/src/AlphaTab.Test/Test/Globals.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.Reflection;
56
using System.Threading;
7+
using AlphaTab.Collections;
68
using Microsoft.VisualStudio.TestTools.UnitTesting;
79

810
namespace AlphaTab.Test;
@@ -34,11 +36,17 @@ public static void Fail(object? message)
3436
Assert.Fail(Convert.ToString(message));
3537
}
3638

37-
internal static AsyncLocal<int> SnapshotAssertionCounter { get; }
38-
static TestGlobals()
39+
private static readonly Dictionary<string, int> SnapshotAssertionCounters = new();
40+
public static string UseSnapshotValue(string baseName, string hint)
3941
{
40-
SnapshotAssertionCounter = new AsyncLocal<int>();
41-
TestMethodAttribute.GlobalBeforeTest += () => { SnapshotAssertionCounter.Value = 0; };
42+
if (!string.IsNullOrEmpty(hint))
43+
{
44+
baseName += $": {hint}";
45+
}
46+
47+
var value = SnapshotAssertionCounters.GetValueOrDefault(baseName) + 1;
48+
SnapshotAssertionCounters[baseName] = value;
49+
return $"{baseName} {value}";
4250
}
4351
}
4452

@@ -245,19 +253,32 @@ public void ToMatchSnapshot(string hint = "")
245253
Assert.Fail("Could not find snapshot file at " + absoluteSnapFilePath);
246254
}
247255

248-
var snapshotFile = SnapshotFileRepository.LoadSnapshortFile(absoluteSnapFilePath);
256+
var snapshotFile = SnapshotFileRepository.LoadSnapshotFile(absoluteSnapFilePath);
249257

250-
var testSuiteName = testMethodInfo.MethodInfo.DeclaringType!.Name;
251-
var testName = testMethodInfo.MethodInfo.GetCustomAttribute<TestMethodAttribute>()!.DisplayName;
258+
var parts = new Collections.List<string>();
259+
CollectTestSuiteNames(parts, testMethodInfo.MethodInfo.DeclaringType!);
260+
var testName = testMethodInfo.MethodInfo.GetCustomAttribute<TestMethodAttribute>()!
261+
.DisplayName;
262+
parts.Add(testName ?? "");
252263

253-
var snapshortName = $"{testSuiteName} {testName} {++TestGlobals.SnapshotAssertionCounter.Value}";
264+
var snapshotName = TestGlobals.UseSnapshotValue(string.Join(" ", parts), hint);
254265

255-
var error = snapshotFile.Match(snapshortName, _actual);
266+
var error = snapshotFile.Match(snapshotName, _actual);
256267
if (!string.IsNullOrEmpty(error))
257268
{
258269
Assert.Fail(error);
259270
}
260271
}
261-
}
262272

273+
private static void CollectTestSuiteNames(Collections.List<string> parts, Type testClass)
274+
{
275+
if (testClass.DeclaringType is not null)
276+
{
277+
CollectTestSuiteNames(parts, testClass.DeclaringType!);
278+
}
263279

280+
var testSuiteName = testClass.GetCustomAttribute<TestClassAttribute>()?.DisplayName ??
281+
testClass.Name;
282+
parts.Add(testSuiteName);
283+
}
284+
}

packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ package alphaTab.core
22

33
import alphaTab.SnapshotFileRepository
44
import alphaTab.TestPlatformPartials
5+
import alphaTab.collections.List
56
import alphaTab.collections.ObjectDoubleMap
67
import org.junit.Assert
78
import java.lang.reflect.Method
89
import java.nio.file.Paths
910
import kotlin.contracts.ExperimentalContracts
1011
import kotlin.reflect.KClass
1112

13+
annotation class TestClass(val name: String)
1214
annotation class TestName(val name: String)
1315
annotation class SnapshotFile(val path: String)
1416

15-
typealias Test = org.junit.Test
17+
1618

1719
class assert {
1820
companion object {
@@ -50,6 +52,7 @@ class NotExpector<T>(private val actual: T) {
5052
class Expector<T>(private val actual: T) {
5153
val to
5254
get() = this
55+
5356
fun not() = NotExpector(actual)
5457

5558
val be
@@ -69,8 +72,9 @@ class Expector<T>(private val actual: T) {
6972
expectedTyped = expected.toInt();
7073
}
7174

72-
if(expected is Double && expected == 0.0 &&
73-
actualToCheck is Double) {
75+
if (expected is Double && expected == 0.0 &&
76+
actualToCheck is Double
77+
) {
7478
val d = actualToCheck as Double;
7579
if (d == -0.0) {
7680
@Suppress("UNCHECKED_CAST")
@@ -84,7 +88,10 @@ class Expector<T>(private val actual: T) {
8488

8589
fun lessThan(expected: Double) {
8690
if (actual is Number) {
87-
Assert.assertTrue("Expected $actual to be less than $expected", actual.toDouble() < expected)
91+
Assert.assertTrue(
92+
"Expected $actual to be less than $expected",
93+
actual.toDouble() < expected
94+
)
8895
} else {
8996
Assert.fail("lessThan can only be used with numeric operands");
9097
}
@@ -93,11 +100,15 @@ class Expector<T>(private val actual: T) {
93100

94101
fun greaterThan(expected: Double) {
95102
if (actual is Number) {
96-
Assert.assertTrue("Expected $actual to be greater than $expected", actual.toDouble() > expected)
103+
Assert.assertTrue(
104+
"Expected $actual to be greater than $expected",
105+
actual.toDouble() > expected
106+
)
97107
} else {
98108
Assert.fail("greaterThan can only be used with numeric operands");
99109
}
100110
}
111+
101112
fun closeTo(expected: Double, delta: Double, message: String? = null) {
102113
if (actual is Number) {
103114
Assert.assertEquals(message, expected, actual.toDouble(), delta)
@@ -106,21 +117,24 @@ class Expector<T>(private val actual: T) {
106117
}
107118
}
108119

109-
fun length(expected:Double) {
110-
if(actual is alphaTab.collections.List<*>) {
120+
fun length(expected: Double) {
121+
if (actual is alphaTab.collections.List<*>) {
111122
Assert.assertEquals(expected.toInt(), actual.length.toInt())
112-
} else if(actual is alphaTab.collections.DoubleList) {
123+
} else if (actual is alphaTab.collections.DoubleList) {
113124
Assert.assertEquals(expected.toInt(), actual.length.toInt())
114-
} else if(actual is alphaTab.collections.BooleanList) {
125+
} else if (actual is alphaTab.collections.BooleanList) {
115126
Assert.assertEquals(expected.toInt(), actual.length.toInt())
116127
} else {
117128
Assert.fail("length can only be used with collection operands");
118129
}
119130
}
120131

121132
fun contain(value: Any) {
122-
if(actual is Iterable<*>) {
123-
Assert.assertTrue("Expected collection ${actual.joinToString(",")} to contain $value", actual.contains(value))
133+
if (actual is Iterable<*>) {
134+
Assert.assertTrue(
135+
"Expected collection ${actual.joinToString(",")} to contain $value",
136+
actual.contains(value)
137+
)
124138
} else {
125139
Assert.fail("contain can only be used with Iterable operands");
126140
}
@@ -180,15 +194,15 @@ class Expector<T>(private val actual: T) {
180194

181195
private fun findTestMethod(): Method {
182196
val walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
183-
var testMethod:Method? = null
197+
var testMethod: Method? = null
184198
walker.forEach { frame ->
185-
if(testMethod == null) {
199+
if (testMethod == null) {
186200
val method = frame.declaringClass.getDeclaredMethod(
187201
frame.methodName,
188202
*frame.methodType.parameterArray()
189203
)
190204

191-
if(method.getAnnotation(Test::class.java) != null) {
205+
if (method.getAnnotation(org.junit.Test::class.java) != null) {
192206
testMethod = method
193207
}
194208
}
@@ -203,7 +217,7 @@ class Expector<T>(private val actual: T) {
203217

204218
@ExperimentalUnsignedTypes
205219
@ExperimentalContracts
206-
fun toMatchSnapshot() {
220+
fun toMatchSnapshot(hint: String = "") {
207221
val testMethodInfo = findTestMethod()
208222
val file = testMethodInfo.getAnnotation(SnapshotFile::class.java)?.path
209223
if (file.isNullOrEmpty()) {
@@ -218,23 +232,35 @@ class Expector<T>(private val actual: T) {
218232
Assert.fail("Could not find snapshot file at $absoluteSnapFilePath")
219233
}
220234

221-
val snapshotFile = SnapshotFileRepository.loadSnapshortFile(absoluteSnapFilePath.toString())
235+
val snapshotFile = SnapshotFileRepository.loadSnapshotFile(absoluteSnapFilePath.toString())
222236

223-
val testSuiteName = testMethodInfo.declaringClass.simpleName
237+
val parts = List<String>();
238+
collectTestSuiteNames(parts, testMethodInfo.declaringClass)
224239
val testName = testMethodInfo.getAnnotation(TestName::class.java)!!.name
240+
parts.push(testName)
241+
242+
val snapshotName = TestGlobals.useSnapshotValue(parts.joinToString(" "), hint);
243+
244+
225245

226-
val fullTestName = "$testSuiteName $testName "
227246

228-
val counter = (TestGlobals.snapshotAssertionCounters.get(fullTestName) ?: 0.0) + 1
229-
TestGlobals.snapshotAssertionCounters.set(fullTestName, counter)
230247

231-
val snapshotName = "$fullTestName${counter.toInt()}"
232248

233249
val error = snapshotFile.match(snapshotName, actual)
234250
if (!error.isNullOrEmpty()) {
235251
Assert.fail(error)
236252
}
237253
}
254+
255+
private fun collectTestSuiteNames(parts: List<String>, testClass: Class<*>) {
256+
if (testClass.declaringClass != null) {
257+
collectTestSuiteNames(parts, testClass.declaringClass!!);
258+
}
259+
260+
val testSuiteName =
261+
testClass.getAnnotation(TestClass::class.java)?.name ?: testClass.simpleName
262+
parts.push(testSuiteName);
263+
}
238264
}
239265

240266
class TestGlobals {
@@ -243,6 +269,18 @@ class TestGlobals {
243269
companion object {
244270
val snapshotAssertionCounters: ObjectDoubleMap<String> = ObjectDoubleMap()
245271

272+
fun useSnapshotValue(baseName: String, hint: String): String {
273+
var fullName = baseName
274+
if (hint.isNotEmpty()) {
275+
fullName += ": $hint";
276+
}
277+
278+
val value =
279+
if (snapshotAssertionCounters.has(fullName)) snapshotAssertionCounters.get(fullName)!! + 1 else 1.0
280+
snapshotAssertionCounters.set(fullName, value)
281+
return "$fullName ${value.toInt()}";
282+
}
283+
246284
fun <T> expect(actual: T): Expector<T> {
247285
return Expector(actual);
248286
}

0 commit comments

Comments
 (0)