@hyperfixi/testing-framework 4.3.0 → 4.5.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 +130 -1
- package/package.json +8 -8
- package/src/multilingual/README.md +35 -0
- package/src/multilingual/command-shapes-gate.ts +32 -0
- package/src/multilingual/command-shapes.ts +22 -23
- package/src/multilingual/direct-path-shapes.ts +60 -26
- package/src/multilingual/english-leaks.corpus.test.ts +74 -0
- package/src/multilingual/english-leaks.test.ts +463 -0
- package/src/multilingual/english-leaks.ts +1000 -0
- package/src/multilingual/hand-written-input.tasks.json +162 -0
- package/src/multilingual/hand-written-input.test.ts +62 -0
- package/src/multilingual/hand-written-input.ts +201 -0
- package/src/vocab/batch3-roundtrip.test.ts +2 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The English-leak scanner (english-leaks.ts) on hand-written renders: each
|
|
3
|
+
* guard is pinned both ways, so a scanner that stopped applying one fails here
|
|
4
|
+
* rather than silently moving the baselines.
|
|
5
|
+
*
|
|
6
|
+
* @vitest-environment node
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
compressLanguages,
|
|
11
|
+
diffLeakBaseline,
|
|
12
|
+
expandLanguages,
|
|
13
|
+
gainedLeaks,
|
|
14
|
+
prunableLeaks,
|
|
15
|
+
findingKey,
|
|
16
|
+
initLeakScanner,
|
|
17
|
+
leakBaselineFrom,
|
|
18
|
+
leakRatesByLanguage,
|
|
19
|
+
type LeakFinding,
|
|
20
|
+
type LeakResults,
|
|
21
|
+
type LeakScanner,
|
|
22
|
+
} from './english-leaks';
|
|
23
|
+
|
|
24
|
+
let scanner: LeakScanner;
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
scanner = await initLeakScanner();
|
|
28
|
+
}, 120_000);
|
|
29
|
+
|
|
30
|
+
/** The finding keys of `render`, a render of English `source` into `language`. */
|
|
31
|
+
function keys(source: string, render: string, language: string): string[] {
|
|
32
|
+
const info = scanner.sourceInfo(source, 'program');
|
|
33
|
+
if (!info) throw new Error(`the engine rejects ${source}`);
|
|
34
|
+
return scanner.scan(render, info, language).map(findingKey).sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The same, of one kind (the spaced markers of tr and ja stand beside their cases). */
|
|
38
|
+
function keysOf(kind: string, source: string, render: string, language: string): string[] {
|
|
39
|
+
return keys(source, render, language).filter(k => k.startsWith(`${kind}:`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findings(source: string, render: string, language: string): LeakFinding[] {
|
|
43
|
+
const info = scanner.sourceInfo(source, 'program');
|
|
44
|
+
if (!info) throw new Error(`the engine rejects ${source}`);
|
|
45
|
+
return scanner.scan(render, info, language);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('english-leaks scanner', () => {
|
|
49
|
+
it('counts an English grammar word the language has its own word for, or not', () => {
|
|
50
|
+
const source = 'on click if I match .a then add .b to me end';
|
|
51
|
+
expect(keys(source, 'al clic si I match .a entonces agregar .b a mí fin', 'es')).toEqual([
|
|
52
|
+
'I',
|
|
53
|
+
'match',
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('never counts a word spelled like the language’s own word (de `in`)', () => {
|
|
58
|
+
const source = 'on click repeat for x in .item add .y to x end';
|
|
59
|
+
// es has no word spelled `in`; de's `in` is its own word.
|
|
60
|
+
expect(keys(source, 'al clic repetir para x in .item agregar .y a x fin', 'es')).toEqual([
|
|
61
|
+
'in',
|
|
62
|
+
]);
|
|
63
|
+
expect(
|
|
64
|
+
keys(source, 'bei klick wiederholen für x in .item hinzufügen .y zu x ende', 'de')
|
|
65
|
+
).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('counts an English alternative the reader still takes, beside the language’s own word', () => {
|
|
69
|
+
// ru's `forever` is `всегда`; the profile lists English `forever` too, so the
|
|
70
|
+
// reader takes code-switched input, but a render that writes it leaks.
|
|
71
|
+
const source = 'on click repeat forever log 1 end';
|
|
72
|
+
expect(keys(source, 'при клик повторить всегда записать 1 конец', 'ru')).toEqual([]);
|
|
73
|
+
expect(keys(source, 'при клик повторить forever записать 1 конец', 'ru')).toEqual(['forever']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('counts a reference the engine reads as a name (`me`)', () => {
|
|
77
|
+
expect(keys('on click log 1, me', 'al clic registrar 1, me', 'es')).toEqual(['me']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('never counts a name, even one spelled like a keyword', () => {
|
|
81
|
+
// `index` is a variable here, a keyword below.
|
|
82
|
+
expect(keys('on click set index to 1', 'al clic establecer index a 1', 'es')).toEqual([]);
|
|
83
|
+
expect(
|
|
84
|
+
keys(
|
|
85
|
+
'on load repeat for x in .item index i add .v to x end',
|
|
86
|
+
'al carga repeat x en .item index i agregar .v a x fin',
|
|
87
|
+
'es'
|
|
88
|
+
)
|
|
89
|
+
).toEqual(['index', 'repeat']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('counts a word no more often than the source uses it as grammar', () => {
|
|
93
|
+
// `index` once as a keyword, once as a name: the render's two count once.
|
|
94
|
+
expect(
|
|
95
|
+
keys(
|
|
96
|
+
'on load repeat for x in .item index index add .v to x end',
|
|
97
|
+
'al carga repetir para x en .item index index agregar .v a x fin',
|
|
98
|
+
'es'
|
|
99
|
+
)
|
|
100
|
+
).toEqual(['index']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not read strings, `js` blocks (with their parameters) or brace interiors', () => {
|
|
104
|
+
expect(
|
|
105
|
+
keys('on click put "repeat in me" into #a', 'al clic poner "repeat in me" en #a', 'es')
|
|
106
|
+
).toEqual([]);
|
|
107
|
+
expect(
|
|
108
|
+
keys(
|
|
109
|
+
'on click js(me) return new Date() end then put it into #a',
|
|
110
|
+
'al clic js (me) return new Date() fin entonces ponerlo en #a',
|
|
111
|
+
'es'
|
|
112
|
+
)
|
|
113
|
+
).toEqual([]);
|
|
114
|
+
expect(
|
|
115
|
+
keys(
|
|
116
|
+
'on click fetch /a with {body: it, method: "POST"}',
|
|
117
|
+
'al clic buscar "/a" con {body: it, method: "POST"}',
|
|
118
|
+
'es'
|
|
119
|
+
)
|
|
120
|
+
).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not read a `js` body or its parameter list, even where they repeat a grammar word', () => {
|
|
124
|
+
// `if` is grammar once (the handler's); the body's `if` is JavaScript.
|
|
125
|
+
expect(
|
|
126
|
+
keys(
|
|
127
|
+
'on click if x then js(x) if (x) return 1 end end',
|
|
128
|
+
'al clic si x entonces js (x) if (x) return 1 fin fin',
|
|
129
|
+
'es'
|
|
130
|
+
)
|
|
131
|
+
).toEqual([]);
|
|
132
|
+
// `me` is a reference once (put's); the parameter list passes it to JavaScript.
|
|
133
|
+
expect(
|
|
134
|
+
keys(
|
|
135
|
+
'on click put me into #a then js(me) return 1 end',
|
|
136
|
+
'al clic ponerme en #a entonces js (me) return 1 fin',
|
|
137
|
+
'es'
|
|
138
|
+
)
|
|
139
|
+
).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("reads a word after a possessive quote (`the element's x`) and not as a string", () => {
|
|
143
|
+
expect(
|
|
144
|
+
keys(
|
|
145
|
+
"on click set the element's x to the element's y",
|
|
146
|
+
"al clic establecer the element's x a the element's y",
|
|
147
|
+
'es'
|
|
148
|
+
)
|
|
149
|
+
).toEqual(['element', 'element', 'the', 'the']);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('gives each word its context: clause, bracket, call, property, plain', () => {
|
|
153
|
+
const clause = findings(
|
|
154
|
+
'on click take .foo from .div for #d3',
|
|
155
|
+
'al clic tomar .foo de .div for #d3',
|
|
156
|
+
'es'
|
|
157
|
+
);
|
|
158
|
+
expect(clause.map(f => `${f.word}/${f.context}`)).toEqual(['for/clause']);
|
|
159
|
+
const bracket = findings(
|
|
160
|
+
'on keyup[key is "Escape"] add .x',
|
|
161
|
+
'al keyup[key is "Escape"] agregar .x',
|
|
162
|
+
'es'
|
|
163
|
+
);
|
|
164
|
+
expect(bracket.map(f => `${f.word}/${f.context}`)).toEqual(['is/bracket']);
|
|
165
|
+
const property = findings(
|
|
166
|
+
"on click put #a's children into #b",
|
|
167
|
+
'bei klick setzen children von #a in #b',
|
|
168
|
+
'de'
|
|
169
|
+
);
|
|
170
|
+
expect(property.map(f => `${f.word}/${f.context}/${f.hasWord}`)).toEqual([
|
|
171
|
+
'children/property/true',
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('counts a property name only where the language has a word for it', () => {
|
|
176
|
+
// innerHTML is a DOM name no lexicon translates.
|
|
177
|
+
expect(
|
|
178
|
+
keys("on click put #a's innerHTML into #b", 'bei klick setzen innerHTML von #a in #b', 'de')
|
|
179
|
+
).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('counts an English event only where the lexicon has a word and the renderer may use it', () => {
|
|
183
|
+
expect(keys('on click add .x', 'при click добавить .x', 'ru')).toEqual(['event:click']);
|
|
184
|
+
expect(keys('on click add .x', 'при клик добавить .x', 'ru')).toEqual([]);
|
|
185
|
+
// A custom event has no word anywhere.
|
|
186
|
+
expect(keys('on foo add .x', 'при foo добавить .x', 'ru')).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('never counts an event the renderer is told to keep English (its denylist)', async () => {
|
|
190
|
+
const { getEventLocalizationDenylist } = await import('@lokascript/semantic');
|
|
191
|
+
const [language, events] = Object.entries(getEventLocalizationDenylist()).find(
|
|
192
|
+
([, e]) => e.size > 0
|
|
193
|
+
)!;
|
|
194
|
+
const event = [...events][0]!;
|
|
195
|
+
const found = keys(`on ${event} add .x`, `${event} add .x`, language).filter(k =>
|
|
196
|
+
k.startsWith('event:')
|
|
197
|
+
);
|
|
198
|
+
expect(found).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('counts a nominative `me` beside a marker, in the languages where that is wrong', () => {
|
|
202
|
+
expect(keys('on click add .b to me', 'al clic agregar .b a yo', 'es')).toEqual(['case:me']);
|
|
203
|
+
expect(keysOf('case', 'on click add .b to me', 'tıklama da .b i ben e ekle', 'tr')).toEqual([
|
|
204
|
+
'case:me',
|
|
205
|
+
]);
|
|
206
|
+
// fr writes the stressed form; ja's particle attaches to any noun.
|
|
207
|
+
expect(keys('on click add .b to me', 'sur clic ajouter .b à moi', 'fr')).toEqual([]);
|
|
208
|
+
expect(keysOf('case', 'on click add .b to me', 'クリック で 自分 に .b を 追加', 'ja')).toEqual(
|
|
209
|
+
[]
|
|
210
|
+
);
|
|
211
|
+
// The oblique forms are right.
|
|
212
|
+
expect(keys('on click add .b to me', 'al clic agregar .b a mí', 'es')).toEqual([]);
|
|
213
|
+
expect(keys('on click add .b to me', 'при клик добавить .b ко мне', 'ru')).toEqual([]);
|
|
214
|
+
// it writes `me` after a marker: the profile's own word, not English's.
|
|
215
|
+
expect(keys('on click put "x" into me', 'su clic mettere "x" in me', 'it')).toEqual([]);
|
|
216
|
+
expect(keys('on click put "x" into me', 'su clic mettere "x" in io', 'it')).toEqual([
|
|
217
|
+
'case:me',
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('counts a nominative `it` beside a marker, where the language has another form', () => {
|
|
222
|
+
const source = 'on click add .b to it';
|
|
223
|
+
expect(keys(source, 'при клик добавить .b к это', 'ru')).toEqual(['case:it']);
|
|
224
|
+
expect(keys(source, 'wenn klick hinzufügen .b zu es', 'de')).toEqual(['case:it']);
|
|
225
|
+
expect(keysOf('case', source, 'tıklama i üzerinde o e .b i ekle', 'tr')).toEqual(['case:it']);
|
|
226
|
+
expect(keys(source, 'при клик добавить .b к этому', 'ru')).toEqual([]);
|
|
227
|
+
// es `ello` and it `esso` are right after a marker.
|
|
228
|
+
expect(keys(source, 'al clic agregar .b a ello', 'es')).toEqual([]);
|
|
229
|
+
expect(keys(source, 'su clic aggiungere .b a esso', 'it')).toEqual([]);
|
|
230
|
+
// pt: only `em` and `de` contract with `ele` (nele, dele); `a ele` is right.
|
|
231
|
+
expect(keys('on click put "x" into it', 'ao clique colocar "x" em ele', 'pt')).toEqual([
|
|
232
|
+
'case:it',
|
|
233
|
+
]);
|
|
234
|
+
expect(keys(source, 'ao clique adicionar .b a ele', 'pt')).toEqual([]);
|
|
235
|
+
// The ru/uk/pl demonstrative's accusative is its nominative: right after a
|
|
236
|
+
// marker that takes the accusative (a direction), wrong after one that never does.
|
|
237
|
+
const into = 'on click put "x" into it';
|
|
238
|
+
expect(keys(into, 'при клик положить "x" в это', 'ru')).toEqual([]);
|
|
239
|
+
expect(keys(into, 'gdy kliknięcie umieść "x" w to', 'pl')).toEqual([]);
|
|
240
|
+
expect(keys(source, 'gdy kliknięcie dodaj .b do to', 'pl')).toEqual(['case:it']);
|
|
241
|
+
expect(keys(source, 'при клік додати .b до це', 'uk')).toEqual(['case:it']);
|
|
242
|
+
// tl `bago ito` (before this) is right; `sa ito` is `dito`.
|
|
243
|
+
expect(keys(source, 'kapag click idagdag .b sa ito', 'tl')).toEqual(['case:it']);
|
|
244
|
+
expect(keys('on click wait 1s', 'kapag click maghintay bago ito', 'tl')).toEqual([]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('does not count a pronoun that opens a parenthesized expression', () => {
|
|
248
|
+
const source = 'on click set $b to (it * 2)';
|
|
249
|
+
expect(keys(source, 'wenn klick setze $b auf (es * 2)', 'de')).toEqual([]);
|
|
250
|
+
expect(keys('on click set $b to it', 'wenn klick setze $b auf es', 'de')).toEqual(['case:it']);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("counts a nominative pronoun as a verb's unmarked object, where that is wrong", () => {
|
|
254
|
+
expect(keys('on click measure me', 'wenn klick messen ich', 'de')).toEqual(['case:me-object']);
|
|
255
|
+
expect(keys('on click show me', 'al clic mostrar yo', 'es')).toEqual(['case:me-object']);
|
|
256
|
+
expect(keys('on click hide it', 'al clic ocultar ello', 'es')).toEqual(['case:it-object']);
|
|
257
|
+
expect(keys('on click hide it', 'quand clic cacher il', 'fr')).toEqual(['case:it-object']);
|
|
258
|
+
expect(keys('on click put me into #o', 'при клик положить я в #o', 'ru')).toEqual([
|
|
259
|
+
'case:me-object',
|
|
260
|
+
]);
|
|
261
|
+
// A verb its patterns write (de `verstecke`; the keyword is `verbergen`).
|
|
262
|
+
expect(keys('on click hide me', 'wenn klick verstecke ich', 'de')).toEqual(['case:me-object']);
|
|
263
|
+
// The right object forms, and the objects that are right in the nominative.
|
|
264
|
+
expect(keys('on click measure me', 'wenn klick messen mich', 'de')).toEqual([]);
|
|
265
|
+
expect(keys('on click hide it', 'wenn klick verstecke es', 'de')).toEqual([]);
|
|
266
|
+
expect(keys('on click hide it', 'при клик скрыть это', 'ru')).toEqual([]);
|
|
267
|
+
// fr writes the clitic before the infinitive (`m'afficher`, `me cacher`), and
|
|
268
|
+
// `me` there is its own word.
|
|
269
|
+
expect(keys('on click show me', 'quand clic afficher moi', 'fr')).toEqual(['case:me-object']);
|
|
270
|
+
expect(keys('on click show me', "quand clic m'afficher", 'fr')).toEqual([]);
|
|
271
|
+
expect(keys('on click hide me', 'quand clic me cacher', 'fr')).toEqual([]);
|
|
272
|
+
expect(keys('on click hide it', 'quand clic le cacher', 'fr')).toEqual([]);
|
|
273
|
+
// A condition is not an object: `if` is no verb.
|
|
274
|
+
expect(
|
|
275
|
+
keys('on click if it is empty hide me end', 'al clic si ello es vacío ocultar yo fin', 'es')
|
|
276
|
+
).toEqual(['case:me-object']);
|
|
277
|
+
// A word before the pronoun that is no verb at all.
|
|
278
|
+
expect(keys('on click put it into #o', 'al clic ello en #o', 'es')).toEqual([]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('counts a marker its language attaches, written apart (N5)', () => {
|
|
282
|
+
const source = 'on click put "x" into #out';
|
|
283
|
+
expect(keys(source, '클릭 할 때 "x" 을 #out 에 넣다', 'ko')).toEqual([
|
|
284
|
+
'spacing:에',
|
|
285
|
+
'spacing:을',
|
|
286
|
+
]);
|
|
287
|
+
expect(keys(source, 'クリック で "x" を #out に 置く', 'ja')).toEqual([
|
|
288
|
+
'spacing:で',
|
|
289
|
+
'spacing:に',
|
|
290
|
+
'spacing:を',
|
|
291
|
+
]);
|
|
292
|
+
expect(keys(source, 'ক্লিক তে "x" কে #out এ রাখুন', 'bn')).toEqual([
|
|
293
|
+
'spacing:এ',
|
|
294
|
+
'spacing:কে',
|
|
295
|
+
'spacing:তে',
|
|
296
|
+
]);
|
|
297
|
+
expect(keys(source, 'tıklama i üzerinde "x" i #out e koy', 'tr')).toEqual([
|
|
298
|
+
'spacing:e',
|
|
299
|
+
'spacing:i',
|
|
300
|
+
'spacing:i',
|
|
301
|
+
]);
|
|
302
|
+
// Attached, nothing; a postposition that is its own word is spaced.
|
|
303
|
+
expect(keys(source, '클릭 할 때 "x"를 #out에 넣다', 'ko')).toEqual([]);
|
|
304
|
+
expect(keys(source, 'クリックで "x"を #outに 置く', 'ja')).toEqual([]);
|
|
305
|
+
expect(keys(source, 'tıklamada "x"\'i #out\'a koy', 'tr')).toEqual([]);
|
|
306
|
+
expect(
|
|
307
|
+
keys('on click hide <p/> in #list', "tıklamada #list içindeki <p/>'yi gizle", 'tr')
|
|
308
|
+
).toEqual([]);
|
|
309
|
+
// A marker that opens a line has nothing to attach to.
|
|
310
|
+
expect(keys(source, 'クリックで "x"を\nに 置く', 'ja')).toEqual([]);
|
|
311
|
+
// Not in a language whose markers are words (es `a`), nor in qu's yet.
|
|
312
|
+
expect(keys(source, 'al clic poner "x" en #out', 'es')).toEqual([]);
|
|
313
|
+
expect(keys(source, 'maykama ñitiy "x" ta #out man churay', 'qu')).toEqual([]);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('english-leaks baseline', () => {
|
|
318
|
+
const finding = (word: string): LeakFinding => ({
|
|
319
|
+
kind: 'word',
|
|
320
|
+
word,
|
|
321
|
+
context: 'plain',
|
|
322
|
+
hasWord: false,
|
|
323
|
+
});
|
|
324
|
+
const results = (byLanguage: Record<string, string[] | null>): LeakResults =>
|
|
325
|
+
new Map([
|
|
326
|
+
[
|
|
327
|
+
'row',
|
|
328
|
+
new Map(Object.entries(byLanguage).map(([l, ws]) => [l, ws ? ws.map(finding) : null])),
|
|
329
|
+
],
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
it('groups languages with the same findings, `*` for all 23', () => {
|
|
333
|
+
const all = expandLanguages('*');
|
|
334
|
+
expect(all).toHaveLength(23);
|
|
335
|
+
expect(compressLanguages(all)).toBe('*');
|
|
336
|
+
const doc = leakBaselineFrom(
|
|
337
|
+
results(Object.fromEntries(all.map(l => [l, l === 'ja' ? [] : ['repeat']]))),
|
|
338
|
+
'test'
|
|
339
|
+
);
|
|
340
|
+
expect(doc.entries.row).toEqual({ repeat: compressLanguages(all.filter(l => l !== 'ja')) });
|
|
341
|
+
expect([doc.renders, doc.leaky, doc.findings, doc.kinds.word.renders]).toEqual([
|
|
342
|
+
23, 22, 22, 22,
|
|
343
|
+
]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('counts a lane with no render as no render, not as a clean one', () => {
|
|
347
|
+
const doc = leakBaselineFrom(results({ es: ['repeat'], fr: [], de: null }), 'test');
|
|
348
|
+
expect([doc.renders, doc.leaky]).toEqual([2, 1]);
|
|
349
|
+
expect(doc.unrendered).toEqual({ row: 'de' });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('reports a lane that was refused and renders now as one to prune, never as gained English', () => {
|
|
353
|
+
const baseline = { entries: { row: { repeat: 'es' } }, unrendered: { row: 'de' } };
|
|
354
|
+
const changes = diffLeakBaseline(results({ es: ['repeat'], de: ['event:click'] }), baseline);
|
|
355
|
+
expect(changes).toEqual([
|
|
356
|
+
{ id: 'row', language: 'de', added: ['event:click'], gone: [], newRender: true },
|
|
357
|
+
]);
|
|
358
|
+
expect(gainedLeaks(changes)).toEqual([]);
|
|
359
|
+
expect(prunableLeaks(changes)).toHaveLength(1);
|
|
360
|
+
// Still unrendered: nothing to say.
|
|
361
|
+
expect(diffLeakBaseline(results({ de: null }), baseline)).toEqual([]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('reports a finding a pair gained, and a listed one that is gone (a lane with no render included)', () => {
|
|
365
|
+
const baseline = { entries: { row: { 'match repeat': 'es', repeat: 'de' } } };
|
|
366
|
+
const changes = diffLeakBaseline(
|
|
367
|
+
results({ es: ['repeat'], de: ['repeat', 'as'], fr: null }),
|
|
368
|
+
baseline
|
|
369
|
+
);
|
|
370
|
+
expect(changes).toEqual([
|
|
371
|
+
{ id: 'row', language: 'es', added: [], gone: ['match'] },
|
|
372
|
+
{ id: 'row', language: 'de', added: ['as'], gone: [] },
|
|
373
|
+
]);
|
|
374
|
+
const noRender = diffLeakBaseline(results({ es: null }), baseline);
|
|
375
|
+
expect(noRender).toEqual([{ id: 'row', language: 'es', added: [], gone: ['match', 'repeat'] }]);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('English a recorded decision keeps', () => {
|
|
380
|
+
it('marks a kept phrase and a kept context, and nothing else', () => {
|
|
381
|
+
const debounced = findings(
|
|
382
|
+
'on input debounced at 300ms log 1',
|
|
383
|
+
'al entrada debounced at 300ms registrar 1',
|
|
384
|
+
'es'
|
|
385
|
+
);
|
|
386
|
+
expect(debounced.map(f => `${f.word}:${f.kept ?? false}`)).toEqual([
|
|
387
|
+
'debounced:true',
|
|
388
|
+
'at:true',
|
|
389
|
+
]);
|
|
390
|
+
// `start` is counted; the API name after it is kept.
|
|
391
|
+
const view = findings(
|
|
392
|
+
'on click start view transition add .a end',
|
|
393
|
+
'al clic start view transition agregar .a fin',
|
|
394
|
+
'es'
|
|
395
|
+
);
|
|
396
|
+
expect(view.map(f => `${f.word}:${f.kept ?? false}`)).toEqual([
|
|
397
|
+
'start:false',
|
|
398
|
+
'view:true',
|
|
399
|
+
'transition:true',
|
|
400
|
+
]);
|
|
401
|
+
const bracket = findings(
|
|
402
|
+
'on keyup[key is "Escape"] add .x',
|
|
403
|
+
'al keyup[key is "Escape"] agregar .x',
|
|
404
|
+
'es'
|
|
405
|
+
);
|
|
406
|
+
expect(bracket.map(f => f.kept)).toEqual([true]);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('M2 exit targets', () => {
|
|
411
|
+
const f = (kind: LeakFinding['kind'], word = 'x'): LeakFinding => ({
|
|
412
|
+
kind,
|
|
413
|
+
word,
|
|
414
|
+
context: 'plain',
|
|
415
|
+
hasWord: false,
|
|
416
|
+
});
|
|
417
|
+
/** 20 renders per language: `n` of them with each listed kind, the rest clean. */
|
|
418
|
+
const rows = (perLanguage: Record<string, LeakFinding['kind'][] | 'none'>): LeakResults => {
|
|
419
|
+
const out: LeakResults = new Map();
|
|
420
|
+
for (let i = 0; i < 20; i++) {
|
|
421
|
+
const byLanguage = new Map<string, LeakFinding[] | null>();
|
|
422
|
+
for (const [language, kinds] of Object.entries(perLanguage)) {
|
|
423
|
+
byLanguage.set(language, kinds === 'none' ? null : i < kinds.length ? [f(kinds[i])] : []);
|
|
424
|
+
}
|
|
425
|
+
out.set(`row${i}`, byLanguage);
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
it('holds each language to the target on its own, by share of renders', () => {
|
|
431
|
+
const rates = leakRatesByLanguage(rows({ es: ['word'], de: ['word', 'word'] }), 'corpus');
|
|
432
|
+
expect(rates.get('es')).toMatchObject({ renders: 20, met: { word: true } });
|
|
433
|
+
expect(rates.get('es')!.rate.word).toBeCloseTo(0.05);
|
|
434
|
+
expect(rates.get('de')!.met.word).toBe(false);
|
|
435
|
+
// The command-shape half allows twice as many.
|
|
436
|
+
expect(leakRatesByLanguage(rows({ de: ['word', 'word'] }), 'shapes').get('de')!.met.word).toBe(
|
|
437
|
+
true
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('allows no event and no pronoun case, and skips a lane with no render', () => {
|
|
442
|
+
const rates = leakRatesByLanguage(rows({ ru: ['event'], pl: ['case'], he: 'none' }), 'shapes');
|
|
443
|
+
expect(rates.get('ru')!.met).toEqual({ word: true, event: false, case: true, spacing: true });
|
|
444
|
+
expect(rates.get('pl')!.met).toEqual({ word: true, event: true, case: false, spacing: true });
|
|
445
|
+
expect(rates.has('he')).toBe(false);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('does not count a kept finding', () => {
|
|
449
|
+
const results: LeakResults = new Map([
|
|
450
|
+
['a', new Map([['es', [{ ...f('word', 'url'), kept: true as const }]]])],
|
|
451
|
+
['b', new Map([['es', [f('word', 'in')]]])],
|
|
452
|
+
]);
|
|
453
|
+
expect(leakRatesByLanguage(results, 'corpus').get('es')!.rate.word).toBe(0.5);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('counts a render once per kind, however many findings it holds', () => {
|
|
457
|
+
const results: LeakResults = new Map([
|
|
458
|
+
['a', new Map([['es', [f('word', 'in'), f('word', 'of')]]])],
|
|
459
|
+
['b', new Map([['es', []]])],
|
|
460
|
+
]);
|
|
461
|
+
expect(leakRatesByLanguage(results, 'corpus').get('es')!.rate.word).toBe(0.5);
|
|
462
|
+
});
|
|
463
|
+
});
|