@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,162 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "t01",
|
|
4
|
+
"ask": "When the button is clicked, toggle the class \"active\" on it.",
|
|
5
|
+
"source": "on click toggle .active on me",
|
|
6
|
+
"fixture": "<button></button>",
|
|
7
|
+
"installOn": "button",
|
|
8
|
+
"event": "click"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "t02",
|
|
12
|
+
"ask": "When the button is clicked, put the text Saved into the element with id out.",
|
|
13
|
+
"source": "on click put \"Saved\" into #out",
|
|
14
|
+
"fixture": "<button></button><div id=\"out\"></div>",
|
|
15
|
+
"installOn": "button",
|
|
16
|
+
"event": "click"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "t03",
|
|
20
|
+
"ask": "When the button is clicked, hide the element with id panel.",
|
|
21
|
+
"source": "on click hide #panel",
|
|
22
|
+
"fixture": "<button></button><div id=\"panel\">x</div>",
|
|
23
|
+
"installOn": "button",
|
|
24
|
+
"event": "click"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "t04",
|
|
28
|
+
"ask": "When the button is clicked, add the class \"open\" to the element with id menu, then remove its class \"closed\".",
|
|
29
|
+
"source": "on click add .open to #menu then remove .closed from #menu",
|
|
30
|
+
"fixture": "<button></button><div id=\"menu\" class=\"closed\"></div>",
|
|
31
|
+
"installOn": "button",
|
|
32
|
+
"event": "click"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "t05",
|
|
36
|
+
"ask": "When the button is clicked, fetch /api/greeting and put the result into the element with id out.",
|
|
37
|
+
"source": "on click fetch /api/greeting then put it into #out",
|
|
38
|
+
"fixture": "<button></button><div id=\"out\"></div>",
|
|
39
|
+
"installOn": "button",
|
|
40
|
+
"event": "click"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "t06",
|
|
44
|
+
"ask": "When the button is clicked: if it has the class \"on\", remove that class; otherwise add it.",
|
|
45
|
+
"source": "on click if I match .on remove .on else add .on end",
|
|
46
|
+
"fixture": "<button class=\"on\"></button>",
|
|
47
|
+
"installOn": "button",
|
|
48
|
+
"event": "click"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "t07",
|
|
52
|
+
"ask": "When the button is clicked, append the text \"x\" to the element with id out three times.",
|
|
53
|
+
"source": "on click repeat 3 times append \"x\" to #out end",
|
|
54
|
+
"fixture": "<button></button><div id=\"out\"></div>",
|
|
55
|
+
"installOn": "button",
|
|
56
|
+
"event": "click"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "t08",
|
|
60
|
+
"ask": "When the button is clicked, send a custom event named refresh to the element with id list, then add the class \"sent\" to the button.",
|
|
61
|
+
"source": "on click send refresh to #list then add .sent to me",
|
|
62
|
+
"fixture": "<button></button><ul id=\"list\"></ul>",
|
|
63
|
+
"installOn": "button",
|
|
64
|
+
"event": "click"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "t09",
|
|
68
|
+
"ask": "When the button is clicked, set the variable $n to 5 and put it into the element with id out.",
|
|
69
|
+
"source": "on click set $n to 5 then put $n into #out",
|
|
70
|
+
"fixture": "<button></button><div id=\"out\"></div>",
|
|
71
|
+
"installOn": "button",
|
|
72
|
+
"event": "click"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "t10",
|
|
76
|
+
"ask": "When the button is clicked, increase the variable $count by one and put it into the element with id out.",
|
|
77
|
+
"source": "on click increment $count then put $count into #out",
|
|
78
|
+
"fixture": "<button></button><div id=\"out\"></div>",
|
|
79
|
+
"installOn": "button",
|
|
80
|
+
"event": "click"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "t11",
|
|
84
|
+
"ask": "Define a behavior named Hider that hides the element when it is clicked (the page installs it on the button with: install Hider).",
|
|
85
|
+
"source": "behavior Hider\n on click hide me end\nend",
|
|
86
|
+
"fixture": "<button></button>",
|
|
87
|
+
"installOn": "button",
|
|
88
|
+
"event": "click"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "t12",
|
|
92
|
+
"ask": "When the input receives input, put its value into the element with id out.",
|
|
93
|
+
"source": "on input put my value into #out",
|
|
94
|
+
"fixture": "<input value=\"hi\"><div id=\"out\"></div>",
|
|
95
|
+
"installOn": "input",
|
|
96
|
+
"event": "input"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "t13",
|
|
100
|
+
"ask": "When the button is clicked, put the value of the input with id field into the element with id out.",
|
|
101
|
+
"source": "on click put the value of #field into #out",
|
|
102
|
+
"fixture": "<button></button><input id=\"field\" value=\"abc\"><div id=\"out\"></div>",
|
|
103
|
+
"installOn": "button",
|
|
104
|
+
"event": "click"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "t14",
|
|
108
|
+
"ask": "When the button is clicked, add the class \"sel\" to the first list item inside the element with id list.",
|
|
109
|
+
"source": "on click add .sel to the first <li/> in #list",
|
|
110
|
+
"fixture": "<button></button><ul id=\"list\"><li>a</li><li>b</li></ul>",
|
|
111
|
+
"installOn": "button",
|
|
112
|
+
"event": "click"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "t15",
|
|
116
|
+
"ask": "When the button is clicked, set its attribute aria-expanded to \"true\".",
|
|
117
|
+
"source": "on click set @aria-expanded to \"true\"",
|
|
118
|
+
"fixture": "<button aria-expanded=\"false\"></button>",
|
|
119
|
+
"installOn": "button",
|
|
120
|
+
"event": "click"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "t16",
|
|
124
|
+
"ask": "When the button is clicked, put the text \"x\" at the end of the element with id out.",
|
|
125
|
+
"source": "on click put \"x\" at end of #out",
|
|
126
|
+
"fixture": "<button></button><div id=\"out\">start</div>",
|
|
127
|
+
"installOn": "button",
|
|
128
|
+
"event": "click"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "t17",
|
|
132
|
+
"ask": "When the button is clicked, for every element with the class \"item\", add the class \"seen\" to it.",
|
|
133
|
+
"source": "on click for item in .item add .seen to item end",
|
|
134
|
+
"fixture": "<button></button><p class=\"item\"></p><p class=\"item\"></p>",
|
|
135
|
+
"installOn": "button",
|
|
136
|
+
"event": "click"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "t18",
|
|
140
|
+
"ask": "When the button is clicked, if there is no element with class \"item\" inside the element with id list, put the text \"empty\" into the element with id out.",
|
|
141
|
+
"source": "on click if no .item in #list put \"empty\" into #out end",
|
|
142
|
+
"fixture": "<button></button><ul id=\"list\"></ul><div id=\"out\"></div>",
|
|
143
|
+
"installOn": "button",
|
|
144
|
+
"event": "click"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "t19",
|
|
148
|
+
"ask": "When the button is clicked, take the class \"selected\" from whichever element with class \"tab\" has it, and give it to the button.",
|
|
149
|
+
"source": "on click take .selected from .tab for me",
|
|
150
|
+
"fixture": "<button class=\"tab\"></button><a class=\"tab selected\"></a>",
|
|
151
|
+
"installOn": "button",
|
|
152
|
+
"event": "click"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "t20",
|
|
156
|
+
"ask": "When the button is clicked, remove the button.",
|
|
157
|
+
"source": "on click remove me",
|
|
158
|
+
"fixture": "<button></button>",
|
|
159
|
+
"installOn": "button",
|
|
160
|
+
"event": "click"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hand-written-input gate: see hand-written-input.ts.
|
|
3
|
+
*
|
|
4
|
+
* @vitest-environment node
|
|
5
|
+
* Required, not a preference: the hosts bind the jsdom window's DOM
|
|
6
|
+
* constructors (see shipped-examples-execution.test.ts).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
formatReport,
|
|
11
|
+
loadInputs,
|
|
12
|
+
loadTasks,
|
|
13
|
+
runHandWritten,
|
|
14
|
+
type HandWrittenReport,
|
|
15
|
+
} from './hand-written-input';
|
|
16
|
+
|
|
17
|
+
describe('hand-written inputs', () => {
|
|
18
|
+
let report: HandWrittenReport;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
report = await runHandWritten();
|
|
22
|
+
console.log(formatReport(report));
|
|
23
|
+
}, 300_000);
|
|
24
|
+
|
|
25
|
+
it('has about twenty tasks, each with a reference, a fixture and an event', () => {
|
|
26
|
+
const tasks = loadTasks();
|
|
27
|
+
expect(tasks.length).toBeGreaterThanOrEqual(18);
|
|
28
|
+
expect(tasks.length).toBeLessThanOrEqual(25);
|
|
29
|
+
for (const t of tasks) {
|
|
30
|
+
expect(t.ask.length).toBeGreaterThan(10);
|
|
31
|
+
expect(t.fixture).toContain('<');
|
|
32
|
+
expect(t.event).toBeTruthy();
|
|
33
|
+
}
|
|
34
|
+
expect(new Set(tasks.map(t => t.id)).size).toBe(tasks.length);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('every reference runs the same on upstream and the engine, and does something', () => {
|
|
38
|
+
const bad = report.references.filter(r => !r.holds);
|
|
39
|
+
expect(bad.map(r => `${r.task}: upstream ${r.want} | engine ${r.eng}`)).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('every input names a task', () => {
|
|
43
|
+
expect(report.unknownTasks).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('every input file is valid JSON of {task, source, writer} rows', () => {
|
|
47
|
+
for (const rows of Object.values(loadInputs())) {
|
|
48
|
+
for (const r of rows) {
|
|
49
|
+
expect(typeof r.task).toBe('string');
|
|
50
|
+
expect(typeof r.source).toBe('string');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// No input is seeded here (a render of ours would measure nothing). Once
|
|
56
|
+
// native speakers' files exist, this is where a verdict is held: a `silent`
|
|
57
|
+
// input is a reader bug (the writer was told nothing), to fix or to list.
|
|
58
|
+
it('no hand-written input is silent', () => {
|
|
59
|
+
const silent = report.results.filter(r => r.verdict === 'silent');
|
|
60
|
+
expect(silent.map(r => `${r.lang} ${r.task} ${r.source}`)).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hand-written-input gate (M2, reliability row 6).
|
|
3
|
+
*
|
|
4
|
+
* Every other multilingual gate reads what OUR renderer writes. This one reads
|
|
5
|
+
* what a native speaker writes: about twenty tasks (`hand-written-input.tasks.json`),
|
|
6
|
+
* each an English reference source with a fixture and an event, and one input
|
|
7
|
+
* file per language under `hand-written-inputs/` holding the sources a native
|
|
8
|
+
* speaker wrote from the task's English description and the language's word
|
|
9
|
+
* list, without seeing our renders. Each input runs AS WRITTEN on
|
|
10
|
+
* `@hyperfixi/engine` with the adapter (direct-path-shapes.ts's hosts), against
|
|
11
|
+
* upstream `_hyperscript` running the English reference.
|
|
12
|
+
*
|
|
13
|
+
* Per task and language the verdict is one of:
|
|
14
|
+
* - `accepted`: the input ran and left the DOM the reference leaves;
|
|
15
|
+
* - `refused`: the adapter refused the input (translate threw: the writer is told);
|
|
16
|
+
* - `silent`: the input ran, or the engine rejected it, with another result.
|
|
17
|
+
*
|
|
18
|
+
* NEVER seed the inputs with our renders: the gate would then measure nothing.
|
|
19
|
+
* With no inputs it reports "no inputs yet" and still checks the references
|
|
20
|
+
* (upstream and the engine run each English source identically).
|
|
21
|
+
*
|
|
22
|
+
* The writer's instructions: `docs-internal/multilingual/HAND_WRITTEN_INPUT.md`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import {
|
|
29
|
+
initDirectPathHosts,
|
|
30
|
+
type DirectPathCase,
|
|
31
|
+
type DirectPathHosts,
|
|
32
|
+
type PairResult,
|
|
33
|
+
} from './direct-path-shapes';
|
|
34
|
+
|
|
35
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
export const TASKS_FILE = path.join(here, 'hand-written-input.tasks.json');
|
|
37
|
+
export const INPUTS_DIR = path.resolve(here, '../../hand-written-inputs');
|
|
38
|
+
|
|
39
|
+
export interface HandWrittenTask extends DirectPathCase {
|
|
40
|
+
/** What the writer is asked to make the page do, in plain English. */
|
|
41
|
+
ask: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TaskRow {
|
|
45
|
+
id: string;
|
|
46
|
+
ask: string;
|
|
47
|
+
source: string;
|
|
48
|
+
fixture: string;
|
|
49
|
+
installOn: string;
|
|
50
|
+
event: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadTasks(): HandWrittenTask[] {
|
|
54
|
+
const rows = JSON.parse(readFileSync(TASKS_FILE, 'utf8')) as TaskRow[];
|
|
55
|
+
return rows.map(t => {
|
|
56
|
+
const behavior = /^behavior\s+(\w+)/.exec(t.source);
|
|
57
|
+
return {
|
|
58
|
+
id: `hand-written#${t.id}`,
|
|
59
|
+
ask: t.ask,
|
|
60
|
+
source: t.source,
|
|
61
|
+
fixture: t.fixture,
|
|
62
|
+
installOn: t.installOn,
|
|
63
|
+
event: t.event,
|
|
64
|
+
languages: [],
|
|
65
|
+
...(behavior ? { behaviorName: behavior[1] } : {}),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface HandWrittenInput {
|
|
71
|
+
/** The task's id as the tasks file spells it (`t01`). */
|
|
72
|
+
task: string;
|
|
73
|
+
/** The hyperscript the writer wrote, in their language. */
|
|
74
|
+
source: string;
|
|
75
|
+
/** Who wrote it (a name or initials; one file may hold several writers). */
|
|
76
|
+
writer?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `hand-written-inputs/<lang>.json`: `[{ "task": "t01", "source": "…", "writer": "…" }]`. */
|
|
80
|
+
export function loadInputs(dir = INPUTS_DIR): Record<string, HandWrittenInput[]> {
|
|
81
|
+
const inputs: Record<string, HandWrittenInput[]> = {};
|
|
82
|
+
if (!existsSync(dir)) return inputs;
|
|
83
|
+
for (const file of readdirSync(dir).sort()) {
|
|
84
|
+
const m = /^([a-z]{2})\.json$/.exec(file);
|
|
85
|
+
if (!m) continue;
|
|
86
|
+
const rows = JSON.parse(readFileSync(path.join(dir, file), 'utf8')) as HandWrittenInput[];
|
|
87
|
+
inputs[m[1]!] = rows.filter(r => typeof r.source === 'string' && r.source.trim() !== '');
|
|
88
|
+
}
|
|
89
|
+
return inputs;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type Verdict = 'accepted' | 'refused' | 'silent';
|
|
93
|
+
|
|
94
|
+
export interface HandWrittenResult {
|
|
95
|
+
task: string;
|
|
96
|
+
lang: string;
|
|
97
|
+
writer: string;
|
|
98
|
+
source: string;
|
|
99
|
+
verdict: Verdict;
|
|
100
|
+
pair: PairResult;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function verdictOf(pair: PairResult): Verdict {
|
|
104
|
+
if (pair.ok) return 'accepted';
|
|
105
|
+
if (pair.english.startsWith('✗')) return 'refused';
|
|
106
|
+
return 'silent';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ReferenceCheck {
|
|
110
|
+
task: string;
|
|
111
|
+
want: string;
|
|
112
|
+
eng: string;
|
|
113
|
+
holds: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface HandWrittenReport {
|
|
117
|
+
references: ReferenceCheck[];
|
|
118
|
+
results: HandWrittenResult[];
|
|
119
|
+
/** Tasks the inputs name that the tasks file does not. */
|
|
120
|
+
unknownTasks: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run every input against its task. `hosts` may be shared with another gate;
|
|
125
|
+
* when omitted, one is opened and closed here.
|
|
126
|
+
*/
|
|
127
|
+
export async function runHandWritten(
|
|
128
|
+
inputs = loadInputs(),
|
|
129
|
+
hosts?: DirectPathHosts
|
|
130
|
+
): Promise<HandWrittenReport> {
|
|
131
|
+
const tasks = loadTasks();
|
|
132
|
+
const own = hosts ?? (await initDirectPathHosts());
|
|
133
|
+
try {
|
|
134
|
+
const references: ReferenceCheck[] = [];
|
|
135
|
+
for (const task of tasks) {
|
|
136
|
+
const r = await own.runCase(task);
|
|
137
|
+
references.push({
|
|
138
|
+
task: task.id,
|
|
139
|
+
want: r.want,
|
|
140
|
+
eng: r.eng,
|
|
141
|
+
holds: !r.want.startsWith('✗') && !/^∅( ‖ ∅)*$/.test(r.want) && r.eng === r.want,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const results: HandWrittenResult[] = [];
|
|
145
|
+
const unknownTasks: string[] = [];
|
|
146
|
+
for (const [lang, rows] of Object.entries(inputs)) {
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const task = tasks.find(t => t.id === `hand-written#${row.task}`);
|
|
149
|
+
if (!task) {
|
|
150
|
+
unknownTasks.push(`${lang}:${row.task}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const pair = await own.runWritten(task, lang, row.source);
|
|
154
|
+
results.push({
|
|
155
|
+
task: task.id,
|
|
156
|
+
lang,
|
|
157
|
+
writer: row.writer ?? '',
|
|
158
|
+
source: row.source,
|
|
159
|
+
verdict: verdictOf(pair),
|
|
160
|
+
pair,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { references, results, unknownTasks };
|
|
165
|
+
} finally {
|
|
166
|
+
if (!hosts) await own.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The report, per task and language, as text. */
|
|
171
|
+
export function formatReport(report: HandWrittenReport): string {
|
|
172
|
+
const lines: string[] = [];
|
|
173
|
+
const bad = report.references.filter(r => !r.holds);
|
|
174
|
+
lines.push(
|
|
175
|
+
`references: ${report.references.length - bad.length}/${report.references.length} run the same on upstream and the engine` +
|
|
176
|
+
(bad.length ? ` (NOT: ${bad.map(r => r.task).join(', ')})` : '')
|
|
177
|
+
);
|
|
178
|
+
if (report.results.length === 0) {
|
|
179
|
+
lines.push('hand-written inputs: no inputs yet (hand-written-inputs/<lang>.json)');
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
const langs = [...new Set(report.results.map(r => r.lang))].sort();
|
|
183
|
+
const counts: Record<Verdict, number> = { accepted: 0, refused: 0, silent: 0 };
|
|
184
|
+
for (const r of report.results) counts[r.verdict]++;
|
|
185
|
+
lines.push(
|
|
186
|
+
`hand-written inputs: ${report.results.length} from ${langs.length} language(s): ` +
|
|
187
|
+
`${counts.accepted} accepted, ${counts.refused} refused, ${counts.silent} silent`
|
|
188
|
+
);
|
|
189
|
+
for (const lang of langs) {
|
|
190
|
+
const rows = report.results.filter(r => r.lang === lang);
|
|
191
|
+
const c: Record<Verdict, number> = { accepted: 0, refused: 0, silent: 0 };
|
|
192
|
+
for (const r of rows) c[r.verdict]++;
|
|
193
|
+
lines.push(` ${lang}: ${c.accepted} accepted, ${c.refused} refused, ${c.silent} silent`);
|
|
194
|
+
for (const r of rows.filter(x => x.verdict !== 'accepted')) {
|
|
195
|
+
lines.push(` ${r.verdict.padEnd(8)} ${r.task.replace('hand-written#', '')} ${JSON.stringify(r.source)}`);
|
|
196
|
+
if (r.verdict === 'silent') lines.push(` got ${r.pair.got} | want ${r.pair.ref}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (report.unknownTasks.length) lines.push(`unknown tasks: ${report.unknownTasks.join(', ')}`);
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
@@ -127,7 +127,8 @@ describe('Batch 3 — wrong-verb class (dict word was another command)', () => {
|
|
|
127
127
|
describe('Batch 3 — it blur (noun form dropped the command patient)', () => {
|
|
128
128
|
it('it: blur command render captures blur.patient', () => {
|
|
129
129
|
const { render, triples: t } = renderAndParse('on keydown[key=="Escape"] blur me', 'it');
|
|
130
|
-
|
|
130
|
+
// The verb, not the noun; with its object attached since M2 N3 (`sfuocarmi`).
|
|
131
|
+
expect(render).toMatch(/sfuocar(?:e|mi)\b/);
|
|
131
132
|
expect(t).toContain('blur.patient=me');
|
|
132
133
|
});
|
|
133
134
|
|