@messagevisor/core 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/jest.config.js +8 -0
- package/lib/benchmark/index.d.ts +2 -0
- package/lib/benchmark/index.js +417 -0
- package/lib/benchmark/index.js.map +1 -0
- package/lib/builder/index.d.ts +70 -0
- package/lib/builder/index.js +831 -0
- package/lib/builder/index.js.map +1 -0
- package/lib/cli/index.d.ts +28 -0
- package/lib/cli/index.js +182 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/config/index.d.ts +61 -0
- package/lib/config/index.js +255 -0
- package/lib/config/index.js.map +1 -0
- package/lib/create/index.d.ts +2 -0
- package/lib/create/index.js +405 -0
- package/lib/create/index.js.map +1 -0
- package/lib/datasource/filesystemAdapter.d.ts +44 -0
- package/lib/datasource/filesystemAdapter.js +424 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -0
- package/lib/datasource/index.d.ts +39 -0
- package/lib/datasource/index.js +96 -0
- package/lib/datasource/index.js.map +1 -0
- package/lib/error.d.ts +6 -0
- package/lib/error.js +49 -0
- package/lib/error.js.map +1 -0
- package/lib/evaluate/cli.d.ts +8 -0
- package/lib/evaluate/cli.js +179 -0
- package/lib/evaluate/cli.js.map +1 -0
- package/lib/evaluate/index.d.ts +10 -0
- package/lib/evaluate/index.js +131 -0
- package/lib/evaluate/index.js.map +1 -0
- package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
- package/lib/examples/coerceExampleIsoDates.js +81 -0
- package/lib/examples/coerceExampleIsoDates.js.map +1 -0
- package/lib/examples/index.d.ts +63 -0
- package/lib/examples/index.js +713 -0
- package/lib/examples/index.js.map +1 -0
- package/lib/exporter/index.d.ts +60 -0
- package/lib/exporter/index.js +610 -0
- package/lib/exporter/index.js.map +1 -0
- package/lib/find-duplicates/index.d.ts +41 -0
- package/lib/find-duplicates/index.js +297 -0
- package/lib/find-duplicates/index.js.map +1 -0
- package/lib/generate-code/index.d.ts +11 -0
- package/lib/generate-code/index.js +157 -0
- package/lib/generate-code/index.js.map +1 -0
- package/lib/generate-code/typescript.d.ts +14 -0
- package/lib/generate-code/typescript.js +307 -0
- package/lib/generate-code/typescript.js.map +1 -0
- package/lib/importer/index.d.ts +64 -0
- package/lib/importer/index.js +1092 -0
- package/lib/importer/index.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/info/index.d.ts +17 -0
- package/lib/info/index.js +132 -0
- package/lib/info/index.js.map +1 -0
- package/lib/init/index.d.ts +30 -0
- package/lib/init/index.js +348 -0
- package/lib/init/index.js.map +1 -0
- package/lib/lint/index.d.ts +1 -0
- package/lib/lint/index.js +6 -0
- package/lib/lint/index.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +7 -0
- package/lib/linter/attributeSchema.js +36 -0
- package/lib/linter/attributeSchema.js.map +1 -0
- package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
- package/lib/linter/checkLocaleCircularDependency.js +42 -0
- package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +3 -0
- package/lib/linter/conditionSchema.js +283 -0
- package/lib/linter/conditionSchema.js.map +1 -0
- package/lib/linter/formatSchema.d.ts +325 -0
- package/lib/linter/formatSchema.js +165 -0
- package/lib/linter/formatSchema.js.map +1 -0
- package/lib/linter/icuStyleLint.d.ts +6 -0
- package/lib/linter/icuStyleLint.js +226 -0
- package/lib/linter/icuStyleLint.js.map +1 -0
- package/lib/linter/index.d.ts +34 -0
- package/lib/linter/index.js +557 -0
- package/lib/linter/index.js.map +1 -0
- package/lib/linter/localeSchema.d.ts +672 -0
- package/lib/linter/localeSchema.js +50 -0
- package/lib/linter/localeSchema.js.map +1 -0
- package/lib/linter/messageSchema.d.ts +35 -0
- package/lib/linter/messageSchema.js +115 -0
- package/lib/linter/messageSchema.js.map +1 -0
- package/lib/linter/printError.d.ts +8 -0
- package/lib/linter/printError.js +41 -0
- package/lib/linter/printError.js.map +1 -0
- package/lib/linter/schema.d.ts +33 -0
- package/lib/linter/schema.js +192 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/segmentSchema.d.ts +8 -0
- package/lib/linter/segmentSchema.js +18 -0
- package/lib/linter/segmentSchema.js.map +1 -0
- package/lib/linter/targetSchema.d.ts +337 -0
- package/lib/linter/targetSchema.js +39 -0
- package/lib/linter/targetSchema.js.map +1 -0
- package/lib/linter/testSchema.d.ts +71 -0
- package/lib/linter/testSchema.js +165 -0
- package/lib/linter/testSchema.js.map +1 -0
- package/lib/linter/zodHelpers.d.ts +2 -0
- package/lib/linter/zodHelpers.js +15 -0
- package/lib/linter/zodHelpers.js.map +1 -0
- package/lib/list/index.d.ts +8 -0
- package/lib/list/index.js +524 -0
- package/lib/list/index.js.map +1 -0
- package/lib/matrix.d.ts +4 -0
- package/lib/matrix.js +66 -0
- package/lib/matrix.js.map +1 -0
- package/lib/promoter/index.d.ts +65 -0
- package/lib/promoter/index.js +1208 -0
- package/lib/promoter/index.js.map +1 -0
- package/lib/prune/index.d.ts +37 -0
- package/lib/prune/index.js +673 -0
- package/lib/prune/index.js.map +1 -0
- package/lib/sets.d.ts +10 -0
- package/lib/sets.js +120 -0
- package/lib/sets.js.map +1 -0
- package/lib/tester/cliFormat.d.ts +8 -0
- package/lib/tester/cliFormat.js +15 -0
- package/lib/tester/cliFormat.js.map +1 -0
- package/lib/tester/index.d.ts +35 -0
- package/lib/tester/index.js +713 -0
- package/lib/tester/index.js.map +1 -0
- package/lib/tester/matrix.d.ts +14 -0
- package/lib/tester/matrix.js +76 -0
- package/lib/tester/matrix.js.map +1 -0
- package/lib/tester/prettyDuration.d.ts +1 -0
- package/lib/tester/prettyDuration.js +30 -0
- package/lib/tester/prettyDuration.js.map +1 -0
- package/lib/tester/printTestResult.d.ts +2 -0
- package/lib/tester/printTestResult.js +32 -0
- package/lib/tester/printTestResult.js.map +1 -0
- package/lib/tester/types.d.ts +29 -0
- package/lib/tester/types.js +3 -0
- package/lib/tester/types.js.map +1 -0
- package/package.json +41 -13
- package/src/benchmark/index.spec.ts +375 -0
- package/src/benchmark/index.ts +433 -0
- package/src/builder/index.spec.ts +822 -0
- package/src/builder/index.ts +920 -0
- package/src/cli/index.spec.ts +54 -0
- package/src/cli/index.ts +150 -0
- package/src/config/index.spec.ts +70 -0
- package/src/config/index.ts +259 -0
- package/src/create/index.spec.ts +272 -0
- package/src/create/index.ts +295 -0
- package/src/datasource/filesystemAdapter.ts +313 -0
- package/src/datasource/index.ts +135 -0
- package/src/error.ts +33 -0
- package/src/evaluate/cli.spec.ts +368 -0
- package/src/evaluate/cli.ts +130 -0
- package/src/evaluate/index.ts +161 -0
- package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
- package/src/examples/coerceExampleIsoDates.ts +98 -0
- package/src/examples/index.spec.ts +453 -0
- package/src/examples/index.ts +854 -0
- package/src/exporter/index.spec.ts +443 -0
- package/src/exporter/index.ts +643 -0
- package/src/find-duplicates/index.spec.ts +289 -0
- package/src/find-duplicates/index.ts +314 -0
- package/src/generate-code/index.ts +92 -0
- package/src/generate-code/typescript.spec.ts +241 -0
- package/src/generate-code/typescript.ts +284 -0
- package/src/importer/index.spec.ts +1101 -0
- package/src/importer/index.ts +1190 -0
- package/src/index.ts +18 -0
- package/src/info/index.ts +67 -0
- package/src/init/index.spec.ts +279 -0
- package/src/init/index.ts +292 -0
- package/src/lint/index.ts +1 -0
- package/src/linter/attributeSchema.ts +38 -0
- package/src/linter/checkLocaleCircularDependency.ts +51 -0
- package/src/linter/conditionSchema.ts +386 -0
- package/src/linter/formatSchema.ts +170 -0
- package/src/linter/icuStyleLint.ts +312 -0
- package/src/linter/index.spec.ts +824 -0
- package/src/linter/index.ts +460 -0
- package/src/linter/localeSchema.ts +70 -0
- package/src/linter/messageSchema.ts +152 -0
- package/src/linter/printError.ts +52 -0
- package/src/linter/schema.ts +230 -0
- package/src/linter/segmentSchema.ts +15 -0
- package/src/linter/targetSchema.ts +50 -0
- package/src/linter/testSchema.spec.ts +405 -0
- package/src/linter/testSchema.ts +239 -0
- package/src/linter/zodHelpers.ts +16 -0
- package/src/list/index.spec.ts +431 -0
- package/src/list/index.ts +463 -0
- package/src/matrix.ts +69 -0
- package/src/promoter/index.spec.ts +584 -0
- package/src/promoter/index.ts +1267 -0
- package/src/prune/index.spec.ts +418 -0
- package/src/prune/index.ts +693 -0
- package/src/sets.ts +74 -0
- package/src/tester/cliFormat.ts +11 -0
- package/src/tester/featurevisorIntegration.spec.ts +101 -0
- package/src/tester/index.spec.ts +577 -0
- package/src/tester/index.ts +679 -0
- package/src/tester/matrix.ts +106 -0
- package/src/tester/prettyDuration.ts +34 -0
- package/src/tester/printTestResult.ts +40 -0
- package/src/tester/types.ts +32 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import { createMessagevisor, type MessageValues } from "@messagevisor/sdk";
|
|
5
|
+
|
|
6
|
+
import type { ProjectConfig } from "../config";
|
|
7
|
+
import type { Datasource } from "../datasource";
|
|
8
|
+
import { buildDatafile, resolveFormats } from "../builder";
|
|
9
|
+
import { evaluateSegment } from "../evaluate";
|
|
10
|
+
import {
|
|
11
|
+
assertProjectSetJsonSelection,
|
|
12
|
+
getProjectSetExecutions,
|
|
13
|
+
getProjectSetRelativeFilePath,
|
|
14
|
+
} from "../sets";
|
|
15
|
+
import { CLI_FORMAT_BOLD, CLI_FORMAT_GREEN, CLI_FORMAT_RED } from "./cliFormat";
|
|
16
|
+
import {
|
|
17
|
+
expandLocaleAssertions,
|
|
18
|
+
expandMessageAssertions,
|
|
19
|
+
expandTargetAssertions,
|
|
20
|
+
expandSegmentAssertions,
|
|
21
|
+
} from "./matrix";
|
|
22
|
+
import { prettyDuration } from "./prettyDuration";
|
|
23
|
+
import { printTestResult } from "./printTestResult";
|
|
24
|
+
import type {
|
|
25
|
+
TestAssertionError,
|
|
26
|
+
TestProjectOptions,
|
|
27
|
+
TestResult,
|
|
28
|
+
TestResultAssertion,
|
|
29
|
+
} from "./types";
|
|
30
|
+
|
|
31
|
+
export interface TestFailure {
|
|
32
|
+
test: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TestProjectSetsOptions extends TestProjectOptions {
|
|
37
|
+
set?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TestProjectResult {
|
|
41
|
+
hasError: boolean;
|
|
42
|
+
results: TestResult[];
|
|
43
|
+
failures: TestFailure[];
|
|
44
|
+
assertionsCount: {
|
|
45
|
+
passed: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
};
|
|
48
|
+
testsCount: {
|
|
49
|
+
passed: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
};
|
|
52
|
+
duration: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function containsSubset(actual: unknown, expected: unknown): boolean {
|
|
60
|
+
if (isPlainObject(expected)) {
|
|
61
|
+
if (!isPlainObject(actual)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Object.keys(expected).every((key) => containsSubset(actual[key], expected[key]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(expected)) {
|
|
69
|
+
return (
|
|
70
|
+
Array.isArray(actual) &&
|
|
71
|
+
expected.length === actual.length &&
|
|
72
|
+
expected.every((value, index) => containsSubset(actual[index], value))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return actual === expected;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeTestValue(value: unknown): unknown {
|
|
80
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
81
|
+
const date = new Date(value);
|
|
82
|
+
|
|
83
|
+
if (!Number.isNaN(date.getTime())) {
|
|
84
|
+
return date;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return value.map(normalizeTestValue);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isPlainObject(value)) {
|
|
93
|
+
return Object.fromEntries(
|
|
94
|
+
Object.entries(value).map(([key, entry]) => [key, normalizeTestValue(entry)]),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createAssertion(description: string): TestResultAssertion {
|
|
102
|
+
return {
|
|
103
|
+
description,
|
|
104
|
+
duration: 0,
|
|
105
|
+
passed: true,
|
|
106
|
+
errors: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function failAssertion(assertion: TestResultAssertion, error: TestAssertionError) {
|
|
111
|
+
assertion.passed = false;
|
|
112
|
+
assertion.errors.push(error);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function shouldRunAssertion(description: string, options: TestProjectOptions) {
|
|
116
|
+
if (!options.assertionPattern) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new RegExp(options.assertionPattern).test(description);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getAssertionPrefix(assertion: { assertionIndex?: number; matrixIndex?: number }) {
|
|
124
|
+
if (typeof assertion.matrixIndex === "number") {
|
|
125
|
+
return `Assertion #${(assertion.assertionIndex || 0) + 1}, matrix #${assertion.matrixIndex + 1}: `;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getMessageAssertionDescription(messageKey: string, assertion: any) {
|
|
132
|
+
if (assertion.description) {
|
|
133
|
+
return assertion.description;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return `${getAssertionPrefix(assertion)}${assertion.target || "web"}/${assertion.locale}: ${messageKey}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getSegmentAssertionDescription(testSegmentKey: string, assertion: any) {
|
|
140
|
+
if (assertion.description) {
|
|
141
|
+
return assertion.description;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return `${getAssertionPrefix(assertion)}${
|
|
145
|
+
assertion.segment || testSegmentKey
|
|
146
|
+
} with ${JSON.stringify(assertion.context || {})}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getTargetAssertionDescription(targetKey: string, assertion: any) {
|
|
150
|
+
if (assertion.description) {
|
|
151
|
+
return assertion.description;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const hasStructureChecks =
|
|
155
|
+
typeof assertion.expectedFormats !== "undefined" ||
|
|
156
|
+
(assertion.expectedToIncludeMessages || []).length > 0 ||
|
|
157
|
+
(assertion.expectedToNotIncludeMessages || []).length > 0;
|
|
158
|
+
const hasEvaluation =
|
|
159
|
+
typeof assertion.rawMessage !== "undefined" || typeof assertion.message !== "undefined";
|
|
160
|
+
|
|
161
|
+
if (hasStructureChecks && hasEvaluation) {
|
|
162
|
+
return `${getAssertionPrefix(assertion)}${targetKey}/${assertion.locale} structure + evaluation`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (hasEvaluation) {
|
|
166
|
+
return `${getAssertionPrefix(assertion)}${targetKey}/${assertion.locale} evaluation`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `${getAssertionPrefix(assertion)}${targetKey}/${assertion.locale}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getLocaleAssertionDescription(localeKey: string, assertion: any) {
|
|
173
|
+
if (assertion.description) {
|
|
174
|
+
return assertion.description;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const targetSuffix = assertion.target ? `/${assertion.target}` : "";
|
|
178
|
+
const hasFormats = typeof assertion.expectedFormats !== "undefined";
|
|
179
|
+
const hasEvaluation =
|
|
180
|
+
typeof assertion.rawMessage !== "undefined" &&
|
|
181
|
+
typeof assertion.expectedTranslation !== "undefined";
|
|
182
|
+
|
|
183
|
+
if (hasFormats && hasEvaluation) {
|
|
184
|
+
return `${getAssertionPrefix(assertion)}${localeKey}${targetSuffix} formats + evaluation`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (hasEvaluation) {
|
|
188
|
+
return `${getAssertionPrefix(assertion)}${localeKey}${targetSuffix} evaluation`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return `${getAssertionPrefix(assertion)}${localeKey}${targetSuffix} formats`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getTestFilePath(projectConfig: ProjectConfig, testKey: string) {
|
|
195
|
+
const extension = (projectConfig.parser as any).extension || "yml";
|
|
196
|
+
const basePath = path.join(
|
|
197
|
+
projectConfig.testsDirectoryPath,
|
|
198
|
+
...testKey.split(projectConfig.namespaceCharacter),
|
|
199
|
+
);
|
|
200
|
+
const specPath = `${basePath}.spec.${extension}`;
|
|
201
|
+
const legacyPath = `${basePath}.${extension}`;
|
|
202
|
+
|
|
203
|
+
return path.relative(process.cwd(), fs.existsSync(specPath) ? specPath : legacyPath);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runTest(
|
|
207
|
+
projectConfig: ProjectConfig,
|
|
208
|
+
datasource: Datasource,
|
|
209
|
+
testKey: string,
|
|
210
|
+
test: any,
|
|
211
|
+
revision: string,
|
|
212
|
+
options: TestProjectOptions,
|
|
213
|
+
): Promise<TestResult | undefined> {
|
|
214
|
+
const startTime = Date.now();
|
|
215
|
+
const type: TestResult["type"] = test.message
|
|
216
|
+
? "message"
|
|
217
|
+
: test.segment
|
|
218
|
+
? "segment"
|
|
219
|
+
: test.target
|
|
220
|
+
? "target"
|
|
221
|
+
: "locale";
|
|
222
|
+
const subject = test.message || test.segment || test.target || test.locale;
|
|
223
|
+
const filePath = getTestFilePath(projectConfig, testKey);
|
|
224
|
+
const result: TestResult = {
|
|
225
|
+
key: testKey,
|
|
226
|
+
filePath,
|
|
227
|
+
type,
|
|
228
|
+
subject,
|
|
229
|
+
duration: 0,
|
|
230
|
+
passed: true,
|
|
231
|
+
assertions: [],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (test.message) {
|
|
235
|
+
for (const rawAssertion of expandMessageAssertions(test.assertions || [])) {
|
|
236
|
+
const description = getMessageAssertionDescription(test.message, rawAssertion);
|
|
237
|
+
if (!shouldRunAssertion(description, options)) continue;
|
|
238
|
+
|
|
239
|
+
const assertionStartTime = Date.now();
|
|
240
|
+
const assertion = createAssertion(description);
|
|
241
|
+
const target = rawAssertion.target || "web";
|
|
242
|
+
const datafile = await buildDatafile(
|
|
243
|
+
projectConfig,
|
|
244
|
+
datasource,
|
|
245
|
+
target,
|
|
246
|
+
rawAssertion.locale,
|
|
247
|
+
revision,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (options.showDatafile) {
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(JSON.stringify(datafile, null, 2));
|
|
253
|
+
console.log("");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const flags = rawAssertion.withFlags || {};
|
|
257
|
+
const variations = rawAssertion.withVariations || {};
|
|
258
|
+
const messagevisor = createMessagevisor({
|
|
259
|
+
datafile,
|
|
260
|
+
context: rawAssertion.context,
|
|
261
|
+
resolveFlag: (featureKey) => flags[featureKey] === true,
|
|
262
|
+
resolveVariation: (experimentKey) => variations[experimentKey],
|
|
263
|
+
modules: projectConfig.modules || [],
|
|
264
|
+
logLevel: "warn",
|
|
265
|
+
});
|
|
266
|
+
const actual = messagevisor.translate(
|
|
267
|
+
test.message,
|
|
268
|
+
normalizeTestValue(rawAssertion.values) as MessageValues<string>,
|
|
269
|
+
{
|
|
270
|
+
context: rawAssertion.context,
|
|
271
|
+
currency: rawAssertion.currency,
|
|
272
|
+
timeZone: rawAssertion.timeZone,
|
|
273
|
+
formats: rawAssertion.formats,
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (actual !== rawAssertion.expectedTranslation) {
|
|
278
|
+
failAssertion(assertion, {
|
|
279
|
+
message: "Translation mismatch",
|
|
280
|
+
expected: rawAssertion.expectedTranslation,
|
|
281
|
+
actual,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
assertion.duration = Date.now() - assertionStartTime;
|
|
286
|
+
result.assertions.push(assertion);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (test.segment) {
|
|
291
|
+
const segmentKeys = await datasource.listSegments();
|
|
292
|
+
const segments = Object.fromEntries(
|
|
293
|
+
await Promise.all(segmentKeys.map(async (key) => [key, await datasource.readSegment(key)])),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
for (const rawAssertion of expandSegmentAssertions(test.assertions || [])) {
|
|
297
|
+
const description = getSegmentAssertionDescription(test.segment, rawAssertion);
|
|
298
|
+
if (!shouldRunAssertion(description, options)) continue;
|
|
299
|
+
|
|
300
|
+
const assertionStartTime = Date.now();
|
|
301
|
+
const assertion = createAssertion(description);
|
|
302
|
+
const actual = evaluateSegment(rawAssertion.segment || test.segment, {
|
|
303
|
+
segments,
|
|
304
|
+
context: rawAssertion.context,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (actual !== rawAssertion.expectedToMatch) {
|
|
308
|
+
failAssertion(assertion, {
|
|
309
|
+
message: "Segment match mismatch",
|
|
310
|
+
expected: rawAssertion.expectedToMatch,
|
|
311
|
+
actual,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
assertion.duration = Date.now() - assertionStartTime;
|
|
316
|
+
result.assertions.push(assertion);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (test.target) {
|
|
321
|
+
for (const rawAssertion of expandTargetAssertions(test.assertions || [])) {
|
|
322
|
+
const description = getTargetAssertionDescription(test.target, rawAssertion);
|
|
323
|
+
if (!shouldRunAssertion(description, options)) continue;
|
|
324
|
+
|
|
325
|
+
const assertionStartTime = Date.now();
|
|
326
|
+
const assertion = createAssertion(description);
|
|
327
|
+
const datafile = await buildDatafile(
|
|
328
|
+
projectConfig,
|
|
329
|
+
datasource,
|
|
330
|
+
test.target,
|
|
331
|
+
rawAssertion.locale,
|
|
332
|
+
revision,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (options.showDatafile) {
|
|
336
|
+
console.log("");
|
|
337
|
+
console.log(JSON.stringify(datafile, null, 2));
|
|
338
|
+
console.log("");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const translationKeys = Object.keys(datafile.translations);
|
|
342
|
+
|
|
343
|
+
for (const messageKey of rawAssertion.expectedToIncludeMessages || []) {
|
|
344
|
+
if (!translationKeys.includes(messageKey)) {
|
|
345
|
+
failAssertion(assertion, {
|
|
346
|
+
message: `Expected datafile to include message "${messageKey}"`,
|
|
347
|
+
expected: true,
|
|
348
|
+
actual: false,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const messageKey of rawAssertion.expectedToNotIncludeMessages || []) {
|
|
354
|
+
if (translationKeys.includes(messageKey)) {
|
|
355
|
+
failAssertion(assertion, {
|
|
356
|
+
message: `Expected datafile to not include message "${messageKey}"`,
|
|
357
|
+
expected: false,
|
|
358
|
+
actual: true,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
rawAssertion.expectedFormats &&
|
|
365
|
+
!containsSubset(datafile.formats || {}, rawAssertion.expectedFormats)
|
|
366
|
+
) {
|
|
367
|
+
failAssertion(assertion, {
|
|
368
|
+
message: "Formats subset mismatch",
|
|
369
|
+
expected: rawAssertion.expectedFormats,
|
|
370
|
+
actual: datafile.formats || {},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (
|
|
375
|
+
typeof rawAssertion.rawMessage !== "undefined" ||
|
|
376
|
+
typeof rawAssertion.message !== "undefined"
|
|
377
|
+
) {
|
|
378
|
+
const messagevisor = createMessagevisor({
|
|
379
|
+
datafile,
|
|
380
|
+
locale: rawAssertion.locale,
|
|
381
|
+
context: rawAssertion.context as any,
|
|
382
|
+
modules: projectConfig.modules || [],
|
|
383
|
+
logLevel: "warn",
|
|
384
|
+
});
|
|
385
|
+
const values = normalizeTestValue(rawAssertion.values) as MessageValues<string>;
|
|
386
|
+
const actual =
|
|
387
|
+
typeof rawAssertion.rawMessage !== "undefined"
|
|
388
|
+
? messagevisor.formatMessage(rawAssertion.rawMessage, values, {
|
|
389
|
+
formats: rawAssertion.formats,
|
|
390
|
+
currency: rawAssertion.currency,
|
|
391
|
+
timeZone: rawAssertion.timeZone,
|
|
392
|
+
})
|
|
393
|
+
: messagevisor.translate<string>(rawAssertion.message, values, {
|
|
394
|
+
context: rawAssertion.context as any,
|
|
395
|
+
formats: rawAssertion.formats,
|
|
396
|
+
currency: rawAssertion.currency,
|
|
397
|
+
timeZone: rawAssertion.timeZone,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (actual !== rawAssertion.expectedTranslation) {
|
|
401
|
+
failAssertion(assertion, {
|
|
402
|
+
message: "Translation mismatch",
|
|
403
|
+
expected: rawAssertion.expectedTranslation,
|
|
404
|
+
actual,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
assertion.duration = Date.now() - assertionStartTime;
|
|
410
|
+
result.assertions.push(assertion);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (test.locale) {
|
|
415
|
+
const [localeKeys, targetKeys] = await Promise.all([
|
|
416
|
+
datasource.listLocales(),
|
|
417
|
+
datasource.listTargets(),
|
|
418
|
+
]);
|
|
419
|
+
const [locales, targets] = await Promise.all([
|
|
420
|
+
Promise.all(localeKeys.map(async (key) => [key, await datasource.readLocale(key)])),
|
|
421
|
+
Promise.all(targetKeys.map(async (key) => [key, await datasource.readTarget(key)])),
|
|
422
|
+
]);
|
|
423
|
+
const localesByKey = Object.fromEntries(locales);
|
|
424
|
+
const targetsByKey = Object.fromEntries(targets);
|
|
425
|
+
|
|
426
|
+
for (const rawAssertion of expandLocaleAssertions(test.assertions || [])) {
|
|
427
|
+
const description = getLocaleAssertionDescription(test.locale, rawAssertion);
|
|
428
|
+
if (!shouldRunAssertion(description, options)) continue;
|
|
429
|
+
|
|
430
|
+
const assertionStartTime = Date.now();
|
|
431
|
+
const assertion = createAssertion(description);
|
|
432
|
+
const target = rawAssertion.target ? targetsByKey[rawAssertion.target] : undefined;
|
|
433
|
+
const formats = resolveFormats(test.locale, localesByKey, target);
|
|
434
|
+
|
|
435
|
+
if (options.showDatafile) {
|
|
436
|
+
console.log("");
|
|
437
|
+
console.log(JSON.stringify(formats || {}, null, 2));
|
|
438
|
+
console.log("");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
rawAssertion.expectedFormats &&
|
|
443
|
+
!containsSubset(formats || {}, rawAssertion.expectedFormats)
|
|
444
|
+
) {
|
|
445
|
+
failAssertion(assertion, {
|
|
446
|
+
message: "Formats subset mismatch",
|
|
447
|
+
expected: rawAssertion.expectedFormats,
|
|
448
|
+
actual: formats || {},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (typeof rawAssertion.rawMessage !== "undefined") {
|
|
453
|
+
const messagevisor = createMessagevisor({
|
|
454
|
+
locale: test.locale,
|
|
455
|
+
context: rawAssertion.context as any,
|
|
456
|
+
defaultFormats: {
|
|
457
|
+
[test.locale]: formats || {},
|
|
458
|
+
},
|
|
459
|
+
modules: projectConfig.modules || [],
|
|
460
|
+
logLevel: "warn",
|
|
461
|
+
});
|
|
462
|
+
const actual = messagevisor.formatMessage(
|
|
463
|
+
rawAssertion.rawMessage,
|
|
464
|
+
normalizeTestValue(rawAssertion.values) as MessageValues,
|
|
465
|
+
{
|
|
466
|
+
formats: rawAssertion.formats,
|
|
467
|
+
currency: rawAssertion.currency,
|
|
468
|
+
timeZone: rawAssertion.timeZone,
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (actual !== rawAssertion.expectedTranslation) {
|
|
473
|
+
failAssertion(assertion, {
|
|
474
|
+
message: "Translation mismatch",
|
|
475
|
+
expected: rawAssertion.expectedTranslation,
|
|
476
|
+
actual,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
assertion.duration = Date.now() - assertionStartTime;
|
|
482
|
+
result.assertions.push(assertion);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
result.passed = result.assertions.every((assertion) => assertion.passed);
|
|
487
|
+
result.duration = Date.now() - startTime;
|
|
488
|
+
|
|
489
|
+
if (result.assertions.length === 0) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export async function testProject(
|
|
497
|
+
projectConfig: ProjectConfig,
|
|
498
|
+
datasource: Datasource,
|
|
499
|
+
options: TestProjectOptions = {},
|
|
500
|
+
): Promise<TestProjectResult> {
|
|
501
|
+
const startTime = Date.now();
|
|
502
|
+
const testKeys = await datasource.listTests();
|
|
503
|
+
const keyPattern = options.keyPattern ? new RegExp(options.keyPattern) : null;
|
|
504
|
+
const revision = await datasource.readRevision();
|
|
505
|
+
const results: TestResult[] = [];
|
|
506
|
+
|
|
507
|
+
for (const testKey of testKeys) {
|
|
508
|
+
if (keyPattern && !keyPattern.test(testKey)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const test = (await datasource.readTest(testKey)) as any;
|
|
513
|
+
const result = await runTest(projectConfig, datasource, testKey, test, revision, options);
|
|
514
|
+
|
|
515
|
+
if (!result) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
results.push(result);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const failures = results.flatMap((result) =>
|
|
523
|
+
result.assertions
|
|
524
|
+
.filter((assertion) => !assertion.passed)
|
|
525
|
+
.flatMap((assertion) =>
|
|
526
|
+
assertion.errors.map((error) => ({
|
|
527
|
+
test: result.key,
|
|
528
|
+
message: `${assertion.description} - ${error.message}`,
|
|
529
|
+
})),
|
|
530
|
+
),
|
|
531
|
+
);
|
|
532
|
+
const passedAssertions = results.reduce(
|
|
533
|
+
(sum, result) => sum + result.assertions.filter((assertion) => assertion.passed).length,
|
|
534
|
+
0,
|
|
535
|
+
);
|
|
536
|
+
const failedAssertions = results.reduce(
|
|
537
|
+
(sum, result) => sum + result.assertions.filter((assertion) => !assertion.passed).length,
|
|
538
|
+
0,
|
|
539
|
+
);
|
|
540
|
+
const passedTests = results.filter((result) => result.passed).length;
|
|
541
|
+
const failedTests = results.filter((result) => !result.passed).length;
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
hasError: failures.length > 0,
|
|
545
|
+
results,
|
|
546
|
+
failures,
|
|
547
|
+
assertionsCount: {
|
|
548
|
+
passed: passedAssertions,
|
|
549
|
+
failed: failedAssertions,
|
|
550
|
+
},
|
|
551
|
+
testsCount: {
|
|
552
|
+
passed: passedTests,
|
|
553
|
+
failed: failedTests,
|
|
554
|
+
},
|
|
555
|
+
duration: Date.now() - startTime,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function testProjectSets(
|
|
560
|
+
projectConfig: ProjectConfig,
|
|
561
|
+
datasource: Datasource,
|
|
562
|
+
options: TestProjectSetsOptions = {},
|
|
563
|
+
): Promise<TestProjectResult> {
|
|
564
|
+
const startTime = Date.now();
|
|
565
|
+
const setExecutions = await getProjectSetExecutions(projectConfig, datasource, options.set);
|
|
566
|
+
const results: TestResult[] = [];
|
|
567
|
+
|
|
568
|
+
for (const execution of setExecutions) {
|
|
569
|
+
const result = await testProject(execution.projectConfig, execution.datasource, options);
|
|
570
|
+
|
|
571
|
+
results.push(
|
|
572
|
+
...result.results.map((testResult) => ({
|
|
573
|
+
...testResult,
|
|
574
|
+
key: projectConfig.sets ? `${execution.set}/${testResult.key}` : testResult.key,
|
|
575
|
+
filePath: projectConfig.sets
|
|
576
|
+
? getProjectSetRelativeFilePath(projectConfig, execution.set, testResult.filePath)
|
|
577
|
+
: testResult.filePath,
|
|
578
|
+
})),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const failures = results.flatMap((result) =>
|
|
583
|
+
result.assertions
|
|
584
|
+
.filter((assertion) => !assertion.passed)
|
|
585
|
+
.flatMap((assertion) =>
|
|
586
|
+
assertion.errors.map((error) => ({
|
|
587
|
+
test: result.key,
|
|
588
|
+
message: `${assertion.description} - ${error.message}`,
|
|
589
|
+
})),
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
const passedAssertions = results.reduce(
|
|
593
|
+
(sum, result) => sum + result.assertions.filter((assertion) => assertion.passed).length,
|
|
594
|
+
0,
|
|
595
|
+
);
|
|
596
|
+
const failedAssertions = results.reduce(
|
|
597
|
+
(sum, result) => sum + result.assertions.filter((assertion) => !assertion.passed).length,
|
|
598
|
+
0,
|
|
599
|
+
);
|
|
600
|
+
const passedTests = results.filter((result) => result.passed).length;
|
|
601
|
+
const failedTests = results.filter((result) => !result.passed).length;
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
hasError: failures.length > 0,
|
|
605
|
+
results,
|
|
606
|
+
failures,
|
|
607
|
+
assertionsCount: {
|
|
608
|
+
passed: passedAssertions,
|
|
609
|
+
failed: failedAssertions,
|
|
610
|
+
},
|
|
611
|
+
testsCount: {
|
|
612
|
+
passed: passedTests,
|
|
613
|
+
failed: failedTests,
|
|
614
|
+
},
|
|
615
|
+
duration: Date.now() - startTime,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function printSummary(result: TestProjectResult, options: TestProjectOptions) {
|
|
620
|
+
if (options.onlyFailures !== true || result.hasError) {
|
|
621
|
+
console.log("\n---");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
console.log("");
|
|
625
|
+
|
|
626
|
+
const testSpecsMessage = `Test specs: ${result.testsCount.passed} passed, ${result.testsCount.failed} failed`;
|
|
627
|
+
const testAssertionsMessage = `Assertions: ${result.assertionsCount.passed} passed, ${result.assertionsCount.failed} failed`;
|
|
628
|
+
|
|
629
|
+
if (result.hasError) {
|
|
630
|
+
console.log(CLI_FORMAT_RED, testSpecsMessage);
|
|
631
|
+
console.log(CLI_FORMAT_RED, testAssertionsMessage);
|
|
632
|
+
} else {
|
|
633
|
+
console.log(CLI_FORMAT_GREEN, testSpecsMessage);
|
|
634
|
+
console.log(CLI_FORMAT_GREEN, testAssertionsMessage);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
console.log(CLI_FORMAT_BOLD, `Time: ${prettyDuration(result.duration)}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export const testPlugin = {
|
|
641
|
+
command: "test",
|
|
642
|
+
handler: async ({ projectConfig, datasource, parsed }: any) => {
|
|
643
|
+
assertProjectSetJsonSelection(projectConfig, parsed.set, parsed.json);
|
|
644
|
+
|
|
645
|
+
const result = await testProjectSets(
|
|
646
|
+
projectConfig,
|
|
647
|
+
datasource,
|
|
648
|
+
parsed as TestProjectSetsOptions,
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
if (parsed.json) {
|
|
652
|
+
console.log(
|
|
653
|
+
parsed.pretty ? JSON.stringify(result.failures, null, 2) : JSON.stringify(result.failures),
|
|
654
|
+
);
|
|
655
|
+
return !result.hasError;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
for (const testResult of result.results) {
|
|
659
|
+
if (parsed.onlyFailures && testResult.passed) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
printTestResult(testResult);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
printSummary(result, parsed as TestProjectOptions);
|
|
667
|
+
|
|
668
|
+
return !result.hasError;
|
|
669
|
+
},
|
|
670
|
+
examples: [
|
|
671
|
+
{ command: "test", description: "run all tests" },
|
|
672
|
+
{ command: "test --keyPattern=pattern", description: "run tests matching key pattern" },
|
|
673
|
+
{ command: "test --assertionPattern=pattern", description: "run assertions matching pattern" },
|
|
674
|
+
{ command: "test --onlyFailures", description: "only print failed tests" },
|
|
675
|
+
{ command: "test --showDatafile", description: "show datafile content for each assertion" },
|
|
676
|
+
{ command: "test --verbose", description: "show verbose test output" },
|
|
677
|
+
{ command: "test --json --pretty", description: "print failures as JSON" },
|
|
678
|
+
],
|
|
679
|
+
};
|