@hyperfixi/testing-framework 3.3.0 → 4.0.1
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 +199 -1
- package/package.json +9 -10
- package/src/agent-bench/README.md +48 -1
- package/src/agent-bench/agent-bench.test.ts +7 -0
- package/src/agent-bench/harness.ts +39 -50
- package/src/multilingual/README.md +30 -11
- package/src/multilingual/bare-render-fidelity.ts +1 -1
- package/src/multilingual/cli.ts +5 -3
- package/src/multilingual/direct-path-shapes-gate.ts +56 -0
- package/src/multilingual/direct-path-shapes.1.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.2.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.3.test.ts +12 -0
- package/src/multilingual/direct-path-shapes.cases.json +2621 -0
- package/src/multilingual/direct-path-shapes.ts +459 -0
- package/src/multilingual/orchestrator.ts +2 -1
- package/src/multilingual/render-fidelity.ts +1 -1
- package/src/multilingual/shipped-examples-execution.test.ts +32 -96
- package/src/multilingual/shipped-examples-execution.ts +31 -121
- package/src/multilingual/shipped-sources-engine.test.ts +19 -19
- package/src/multilingual/shipped-sources-localized.test.ts +87 -0
- package/src/multilingual/shipped-sources-validity.ts +87 -99
- package/src/multilingual/validators/execution-validator.test.ts +81 -23
- package/src/multilingual/validators/execution-validator.ts +135 -91
- package/src/multilingual/validators/parse-validator.ts +7 -20
- package/src/multilingual/value-matrix.accepted.test.ts +8 -14
- package/src/multilingual/value-matrix.isolation.test.ts +7 -6
- package/src/multilingual/value-matrix.ts +32 -84
- package/src/multilingual/shipped-sources-validity.test.ts +0 -95
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shipped
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `ok === true` with a non-empty `errors`.
|
|
2
|
+
* Shipped sources: the hyperscript this repository ships to users
|
|
3
|
+
* ----------------------------------------------------------------
|
|
4
|
+
* Collects every hyperscript source in `examples/` and the doc trees, for the
|
|
5
|
+
* gates that read them:
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
* meant, and a `*--css-var` neither engine supports). The same sweep is what
|
|
15
|
-
* caught the `send`-vs-`trigger` divergence and the `parseTriggerCommand` hang
|
|
16
|
-
* in #780, neither of which was reachable from the existing tests.
|
|
7
|
+
* - `shipped-sources-engine.test.ts`: every English source must parse on
|
|
8
|
+
* `@hyperfixi/engine`, or be listed with a reason (`checkShippedSourcesOnEngine`);
|
|
9
|
+
* - `shipped-sources-localized.test.ts`: every source written in another
|
|
10
|
+
* language must translate to English both engines parse (`collectLocalizedSources`);
|
|
11
|
+
* - the R4-style second opinion, upstream `hyperscript.org`, is
|
|
12
|
+
* `loadCanonicalParser()` in canonical-validity.ts.
|
|
17
13
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
14
|
+
* (The file is named for the gate it began with: core's "recovers with
|
|
15
|
+
* errors" ratchet, which flagged sources `@hyperfixi/core`'s resilient parser
|
|
16
|
+
* accepted with diagnostics — how five malformed examples were found, and the
|
|
17
|
+
* `send`-vs-`trigger` divergence and `parseTriggerCommand` hang of #780. It left
|
|
18
|
+
* with core's parser in Phase C4; the engine has no recovering mode, so a
|
|
19
|
+
* malformed source is simply rejected, and the engine gate sees it.)
|
|
24
20
|
*
|
|
25
|
-
* Node-only: the
|
|
26
|
-
* imports the real `hyperscript.org` build off disk. Cannot run in a browser
|
|
27
|
-
* suite.
|
|
21
|
+
* Node-only: walks the repo and reads git's index.
|
|
28
22
|
*/
|
|
29
23
|
|
|
30
24
|
import fs from 'node:fs';
|
|
@@ -32,6 +26,7 @@ import path from 'node:path';
|
|
|
32
26
|
import { execFileSync } from 'node:child_process';
|
|
33
27
|
import { createHash } from 'node:crypto';
|
|
34
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { JSDOM } from 'jsdom';
|
|
35
30
|
import { extractHyperscriptFromMarkup } from '@hyperfixi/patterns-reference';
|
|
36
31
|
|
|
37
32
|
/** Repo root, from `packages/testing-framework/src/multilingual/`. */
|
|
@@ -60,30 +55,6 @@ export interface ShippedSource {
|
|
|
60
55
|
source: string;
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
export interface ShippedSourceFinding extends ShippedSource {
|
|
64
|
-
/** Stable allowlist key: file plus a hash of the source. */
|
|
65
|
-
key: string;
|
|
66
|
-
/** First hyperfixi diagnostic. */
|
|
67
|
-
error: string;
|
|
68
|
-
/** Single-line excerpt, for reading the baseline without opening the file. */
|
|
69
|
-
excerpt: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface ShippedSourcesResult {
|
|
73
|
-
/** Sources extracted and compiled. */
|
|
74
|
-
checked: number;
|
|
75
|
-
/** Sources that compiled with no diagnostics. */
|
|
76
|
-
clean: number;
|
|
77
|
-
/** Sources in the recovers-with-errors state. */
|
|
78
|
-
findings: ShippedSourceFinding[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Minimal shape of `hyperscript.compileSync`, injected so this stays testable. */
|
|
82
|
-
export type CompileForValidity = (code: string) => {
|
|
83
|
-
ok: boolean;
|
|
84
|
-
errors?: Array<{ message: string }>;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
58
|
/**
|
|
88
59
|
* A stable key for an individual snippet.
|
|
89
60
|
*
|
|
@@ -99,7 +70,7 @@ function keyFor(file: string, source: string): string {
|
|
|
99
70
|
/**
|
|
100
71
|
* The files git actually tracks under `repoRoot`, as absolute paths.
|
|
101
72
|
*
|
|
102
|
-
*
|
|
73
|
+
* These gates walk the working TREE, and a working tree is not a clean checkout:
|
|
103
74
|
* `examples/vite-plugin-multilingual/` is GITIGNORED, so a local run saw a
|
|
104
75
|
* source CI could never see. That is unfixable from the allowlist — with an
|
|
105
76
|
* entry for it the gate failed in CI as a STALE entry, and without one it
|
|
@@ -196,45 +167,58 @@ export function collectShippedSources(
|
|
|
196
167
|
return out;
|
|
197
168
|
}
|
|
198
169
|
|
|
170
|
+
/** A shipped source written in another language: an `_` under a non-English `lang`. */
|
|
171
|
+
export interface LocalizedSource extends ShippedSource {
|
|
172
|
+
/** The base language of the closest `lang` (`es` for `es-MX`). */
|
|
173
|
+
lang: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
199
176
|
/**
|
|
200
|
-
*
|
|
177
|
+
* The shipped `_` attributes written in a language other than English. The
|
|
178
|
+
* other collectors hand every source to an English parser, which rejects
|
|
179
|
+
* these, so both shipped-sources gates skip them; on a page they run through
|
|
180
|
+
* the multilingual adapter, which translates a script under its element's
|
|
181
|
+
* `lang`. Pages are parsed as documents here, so a `lang` on `<html>` counts.
|
|
201
182
|
*/
|
|
202
|
-
export function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
183
|
+
export function collectLocalizedSources(opts?: {
|
|
184
|
+
roots?: string[];
|
|
185
|
+
repoRoot?: string;
|
|
186
|
+
}): LocalizedSource[] {
|
|
187
|
+
const repoRoot = opts?.repoRoot ?? REPO_ROOT;
|
|
188
|
+
const roots = opts?.roots ?? DEFAULT_ROOTS;
|
|
189
|
+
const out: LocalizedSource[] = [];
|
|
190
|
+
const tracked = trackedFiles(repoRoot);
|
|
191
|
+
for (const root of roots) {
|
|
192
|
+
for (const full of walk(path.join(repoRoot, root), [], tracked)) {
|
|
193
|
+
const rel = path.relative(repoRoot, full);
|
|
194
|
+
if (EXCLUDED.some(e => e.match(rel))) continue;
|
|
195
|
+
const text = fs.readFileSync(full, 'utf8');
|
|
196
|
+
const kind: ShippedSource['kind'] = full.endsWith('.html')
|
|
197
|
+
? 'html-attribute'
|
|
198
|
+
: 'markdown-html-block';
|
|
199
|
+
const parts =
|
|
200
|
+
kind === 'html-attribute'
|
|
201
|
+
? [text]
|
|
202
|
+
: [...text.matchAll(/```html\n([\s\S]*?)```/g)].map(m => m[1] ?? '');
|
|
203
|
+
for (const html of parts) {
|
|
204
|
+
const { window } = new JSDOM(html);
|
|
205
|
+
const elements: Element[] = Array.from(window.document.querySelectorAll('[_]'));
|
|
206
|
+
for (const el of elements) {
|
|
207
|
+
const source = el.getAttribute('_') ?? '';
|
|
208
|
+
const lang = (el.closest('[lang]')?.getAttribute('lang') ?? '').split('-')[0];
|
|
209
|
+
if (!source.trim() || !lang || lang.toLowerCase() === 'en') continue;
|
|
210
|
+
out.push({ file: rel, kind, source, lang: lang.toLowerCase() });
|
|
211
|
+
}
|
|
212
|
+
window.close();
|
|
213
|
+
}
|
|
229
214
|
}
|
|
230
215
|
}
|
|
231
|
-
|
|
232
|
-
return { checked: sources.length, clean, findings };
|
|
216
|
+
return out;
|
|
233
217
|
}
|
|
234
218
|
|
|
235
|
-
/** A shipped source
|
|
219
|
+
/** A shipped English source `@hyperfixi/engine` rejects. */
|
|
236
220
|
export interface EngineRejection extends ShippedSource {
|
|
237
|
-
/** Stable key: file plus a hash of the source
|
|
221
|
+
/** Stable key: file plus a hash of the source. */
|
|
238
222
|
key: string;
|
|
239
223
|
/** The engine's first parse error, first line. */
|
|
240
224
|
error: string;
|
|
@@ -242,46 +226,50 @@ export interface EngineRejection extends ShippedSource {
|
|
|
242
226
|
}
|
|
243
227
|
|
|
244
228
|
export interface ShippedSourcesOnEngineResult {
|
|
245
|
-
/** Distinct (file, source) pairs
|
|
246
|
-
|
|
229
|
+
/** Distinct (file, source) pairs written in English: the denominator. */
|
|
230
|
+
checked: number;
|
|
247
231
|
/** Of those, the ones the engine parses. */
|
|
248
232
|
engineAccepts: number;
|
|
233
|
+
/** Distinct sources skipped because they are written in another language. */
|
|
234
|
+
localized: number;
|
|
249
235
|
rejections: EngineRejection[];
|
|
250
236
|
}
|
|
251
237
|
|
|
252
238
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
239
|
+
* Every shipped source written in English, put to the engine's parser.
|
|
240
|
+
* `engineErrors` returns the engine's parse errors ([] = it parses).
|
|
241
|
+
*
|
|
242
|
+
* A source under a non-English `lang` is not English hyperscript: on a page it
|
|
243
|
+
* runs through the multilingual adapter, and `shipped-sources-localized.test.ts`
|
|
244
|
+
* checks it that way. Every other source must parse, or be listed.
|
|
256
245
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
246
|
+
* (Until Phase C4 the denominator was the sources `@hyperfixi/core` compiled
|
|
247
|
+
* clean, which also excluded non-English snippets that carried no `lang` and
|
|
248
|
+
* malformed doc examples. Moving off core surfaced fourteen: eleven examples in
|
|
249
|
+
* another language without a `lang`, now marked, and three malformed ones, now
|
|
250
|
+
* fixed.)
|
|
260
251
|
*/
|
|
261
252
|
export function checkShippedSourcesOnEngine(
|
|
262
|
-
compile: CompileForValidity,
|
|
263
253
|
engineErrors: (code: string) => string[],
|
|
264
254
|
doc: Parameters<typeof extractHyperscriptFromMarkup>[0],
|
|
265
255
|
opts?: { roots?: string[]; repoRoot?: string }
|
|
266
256
|
): ShippedSourcesOnEngineResult {
|
|
257
|
+
const foreign = new Set(collectLocalizedSources(opts).map(s => keyFor(s.file, s.source)));
|
|
267
258
|
const seen = new Set<string>();
|
|
268
259
|
const rejections: EngineRejection[] = [];
|
|
269
|
-
let
|
|
260
|
+
let checked = 0;
|
|
270
261
|
let engineAccepts = 0;
|
|
262
|
+
let localized = 0;
|
|
271
263
|
|
|
272
264
|
for (const s of collectShippedSources(doc, opts)) {
|
|
273
265
|
const key = keyFor(s.file, s.source);
|
|
274
266
|
if (seen.has(key)) continue;
|
|
275
267
|
seen.add(key);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
clean = result.ok && (result.errors ?? []).length === 0;
|
|
280
|
-
} catch {
|
|
281
|
-
/* not core-clean */
|
|
268
|
+
if (foreign.has(key)) {
|
|
269
|
+
localized++;
|
|
270
|
+
continue;
|
|
282
271
|
}
|
|
283
|
-
|
|
284
|
-
coreClean++;
|
|
272
|
+
checked++;
|
|
285
273
|
const errors = engineErrors(s.source);
|
|
286
274
|
if (errors.length === 0) {
|
|
287
275
|
engineAccepts++;
|
|
@@ -294,5 +282,5 @@ export function checkShippedSourcesOnEngine(
|
|
|
294
282
|
excerpt: s.source.replace(/\s+/g, ' ').trim().slice(0, 100),
|
|
295
283
|
});
|
|
296
284
|
}
|
|
297
|
-
return {
|
|
285
|
+
return { checked, engineAccepts, localized, rejections };
|
|
298
286
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* R2 — execution smoke validator locks (session 5).
|
|
3
3
|
*
|
|
4
|
+
* @vitest-environment node
|
|
5
|
+
* The node environment is required: under the suite default (happy-dom) the
|
|
6
|
+
* DOM constructors are happy-dom's, the per-execution jsdom's elements fail
|
|
7
|
+
* the hosts' `instanceof` checks, and every handler installs nothing.
|
|
8
|
+
*
|
|
4
9
|
* Locks three things:
|
|
5
10
|
* 1. The curated subset's MEMBERSHIP — expanding/shrinking it recalibrates
|
|
6
11
|
* every language's avgExecutionFidelity, so it must be a deliberate,
|
|
7
12
|
* baseline-regenerating change, never an accident.
|
|
8
|
-
* 2. The en references EXECUTE:
|
|
9
|
-
* the
|
|
10
|
-
* deterministic DOM-effect signature
|
|
11
|
-
* scored against).
|
|
13
|
+
* 2. The en references EXECUTE: upstream `_hyperscript` (the oracle) and
|
|
14
|
+
* `@hyperfixi/engine` each install the handler in jsdom, dispatch produces
|
|
15
|
+
* a non-empty, deterministic DOM-effect signature, and the two agree (the
|
|
16
|
+
* foundation every language is scored against).
|
|
12
17
|
* 3. Known cross-language behavior: a faithful translation reproduces the en
|
|
13
18
|
* signature exactly; a translation that drops the destination role lands
|
|
14
19
|
* its effect on `me` and MUST diverge (the failure class R2 exists to
|
|
@@ -18,6 +23,13 @@
|
|
|
18
23
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
19
24
|
import { ExecutionValidator, EXECUTION_SUBSET, loadExecutionSubset } from './execution-validator';
|
|
20
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Sources loaded off the db for the curated subset: en only, which
|
|
28
|
+
* `db:init:force` writes in CI before this suite runs (translations need a
|
|
29
|
+
* `populate`, which this suite does not have).
|
|
30
|
+
*/
|
|
31
|
+
let enSources: Map<string, string>;
|
|
32
|
+
|
|
21
33
|
describe('R2 execution subset (lock)', () => {
|
|
22
34
|
it('contains exactly the 49 curated patterns', () => {
|
|
23
35
|
// Changing this list recalibrates avgExecutionFidelity for every language.
|
|
@@ -142,6 +154,43 @@ describe('R2 execution validator (lock)', () => {
|
|
|
142
154
|
|
|
143
155
|
beforeAll(async () => {
|
|
144
156
|
await validator.initialize();
|
|
157
|
+
enSources = (await loadExecutionSubset(['en'])).get('en')!;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('every en reference runs on the engine exactly as on upstream, with an effect', async () => {
|
|
161
|
+
// The oracle check behind the reference: the orchestrator scores each
|
|
162
|
+
// translation (on the engine) against upstream's English. An en reference
|
|
163
|
+
// the engine runs differently would fail that pattern in every language
|
|
164
|
+
// at once — this names the engine instead. An empty or erroring reference
|
|
165
|
+
// is excluded from scoring, so it would silently shrink the subset.
|
|
166
|
+
const report: string[] = [];
|
|
167
|
+
for (const id of EXECUTION_SUBSET) {
|
|
168
|
+
const code = enSources.get(id);
|
|
169
|
+
if (!code) continue; // the loadExecutionSubset test below names it
|
|
170
|
+
const up = await validator.execute(id, code, 'en', 'upstream');
|
|
171
|
+
const eng = await validator.execute(id, code, 'en');
|
|
172
|
+
if (up.effects.length === 0)
|
|
173
|
+
report.push(`${id}: no effect on upstream (${up.error ?? 'no error'})`);
|
|
174
|
+
else if (up.error) report.push(`${id}: upstream reports ${up.error}`);
|
|
175
|
+
else if (JSON.stringify(eng.effects) !== JSON.stringify(up.effects)) {
|
|
176
|
+
report.push(
|
|
177
|
+
`${id}:\n upstream ${up.effects.join(' | ')}\n engine ${eng.effects.join(' | ')}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
expect(report, report.join('\n')).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('upstream swaps the two elements: the signature records the order', async () => {
|
|
185
|
+
// `swap #a with #b` changes no class, attribute, style or text — only the
|
|
186
|
+
// two elements' places, which the order line carries.
|
|
187
|
+
const res = await validator.execute(
|
|
188
|
+
'swap-content',
|
|
189
|
+
'on click swap #a with #b',
|
|
190
|
+
'en',
|
|
191
|
+
'upstream'
|
|
192
|
+
);
|
|
193
|
+
expect(res.effects.at(-1)).toMatch(/^order\[.*,b,a\]$/);
|
|
145
194
|
});
|
|
146
195
|
|
|
147
196
|
it('en reference executes: on click add .highlight to me', async () => {
|
|
@@ -206,13 +255,13 @@ describe('R2 execution validator (lock)', () => {
|
|
|
206
255
|
|
|
207
256
|
it('the announce-screen-reader cell executes via its custom-event trigger', async () => {
|
|
208
257
|
// First non-click cell: `on success put event.detail.message into
|
|
209
|
-
// #sr-announce set @role to "alert"
|
|
258
|
+
// #sr-announce set @role of #sr-announce to "alert"`. PATTERN_TRIGGER
|
|
210
259
|
// dispatches a `success` CustomEvent carrying detail.message; the handler
|
|
211
260
|
// writes that text into #sr-announce and sets role=alert on it. Locks both
|
|
212
261
|
// the per-cell trigger wiring and the two-line effect signature.
|
|
213
262
|
const res = await validator.execute(
|
|
214
263
|
'announce-screen-reader',
|
|
215
|
-
'on success put event.detail.message into #sr-announce set @role to "alert"
|
|
264
|
+
'on success put event.detail.message into #sr-announce set @role of #sr-announce to "alert"',
|
|
216
265
|
'en'
|
|
217
266
|
);
|
|
218
267
|
expect(res.error).toBeUndefined();
|
|
@@ -261,11 +310,13 @@ describe('R2 execution validator (lock)', () => {
|
|
|
261
310
|
it('wave-6 en references execute with their locked signatures', async () => {
|
|
262
311
|
// The six wave-6 additions. Each en reference must produce a non-empty,
|
|
263
312
|
// deterministic signature against the existing fixture (the foundation every
|
|
264
|
-
// language is scored against). next/closest
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
313
|
+
// language is scored against). A next/closest positional with no match
|
|
314
|
+
// throws on upstream (core fell back to `me`), so PATTERN_SETUP gives
|
|
315
|
+
// toggle-aria-expanded a following .panel and accordion-toggle an
|
|
316
|
+
// enclosing item; `toggle @attr` writes upstream's `undefined`; set
|
|
317
|
+
// *opacity/*transform write inline style; caret-var-on-target writes
|
|
318
|
+
// `null` into #btn (the undefined `^count`), as upstream's put writes a
|
|
319
|
+
// null value.
|
|
269
320
|
const cases: ReadonlyArray<[string, string, string[]]> = [
|
|
270
321
|
[
|
|
271
322
|
'next-element',
|
|
@@ -275,7 +326,10 @@ describe('R2 execution validator (lock)', () => {
|
|
|
275
326
|
[
|
|
276
327
|
'toggle-aria-expanded',
|
|
277
328
|
'on click toggle @aria-expanded on me toggle .open on next .panel',
|
|
278
|
-
[
|
|
329
|
+
[
|
|
330
|
+
'Δ#btn cls[] attr[aria-expanded=undefined,id=btn] style[] text[Click]',
|
|
331
|
+
'Δ#panel cls[open panel] attr[id=panel] style[] text[]',
|
|
332
|
+
],
|
|
279
333
|
],
|
|
280
334
|
[
|
|
281
335
|
'set-opacity',
|
|
@@ -290,7 +344,10 @@ describe('R2 execution validator (lock)', () => {
|
|
|
290
344
|
[
|
|
291
345
|
'accordion-toggle',
|
|
292
346
|
'on click toggle .open on closest .accordion-item toggle @aria-expanded',
|
|
293
|
-
[
|
|
347
|
+
[
|
|
348
|
+
'Δ#btn cls[] attr[aria-expanded=undefined,id=btn] style[] text[Click]',
|
|
349
|
+
'Δdiv[0] cls[accordion-item card open] attr[] style[] text[]',
|
|
350
|
+
],
|
|
294
351
|
],
|
|
295
352
|
[
|
|
296
353
|
'caret-var-on-target',
|
|
@@ -368,11 +425,10 @@ describe('R2 execution validator (lock)', () => {
|
|
|
368
425
|
it('wave-9 en references execute with their locked signatures', async () => {
|
|
369
426
|
// The three wave-9 additions. Each en reference must produce a non-empty,
|
|
370
427
|
// deterministic signature against the existing fixture. The `*opacity`
|
|
371
|
-
// hide/show STRATEGIES are synchronous (no timer)
|
|
372
|
-
// on #btn
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// chained-access-possessive-dot writes the parent (.card) display.
|
|
428
|
+
// hide/show STRATEGIES are synchronous (no timer) and, on upstream, write
|
|
429
|
+
// the opacity itself: 0 on #btn, 1 on #modal (core ignored the strategy
|
|
430
|
+
// and toggled display, OPEN_ITEMS C1). chained-access-possessive-dot
|
|
431
|
+
// writes the parent (.card) display.
|
|
376
432
|
const cases: ReadonlyArray<[string, string, string[]]> = [
|
|
377
433
|
[
|
|
378
434
|
'chained-access-possessive-dot',
|
|
@@ -382,12 +438,12 @@ describe('R2 execution validator (lock)', () => {
|
|
|
382
438
|
[
|
|
383
439
|
'hide-with-transition',
|
|
384
440
|
'on click hide me with *opacity',
|
|
385
|
-
['Δ#btn cls[] attr[id=btn] style[
|
|
441
|
+
['Δ#btn cls[] attr[id=btn] style[opacity: 0;] text[Click]'],
|
|
386
442
|
],
|
|
387
443
|
[
|
|
388
444
|
'show-with-transition',
|
|
389
445
|
'on click show #modal with *opacity',
|
|
390
|
-
['Δ#modal cls[
|
|
446
|
+
['Δ#modal cls[] attr[id=modal] style[opacity: 1;] text[]'],
|
|
391
447
|
],
|
|
392
448
|
];
|
|
393
449
|
for (const [id, code, expected] of cases) {
|
|
@@ -403,7 +459,9 @@ describe('R2 execution validator (lock)', () => {
|
|
|
403
459
|
// pre-existing document-order indices are preserved). The ja translations
|
|
404
460
|
// lock the exact failure class each fix closed: append's fronted content
|
|
405
461
|
// literal (was a bogus `event` role → runtime "append requires content")
|
|
406
|
-
// and increment's
|
|
462
|
+
// and increment's bare amount (was dropped → +1 instead of +10). The
|
|
463
|
+
// increment row counts in `#score's textContent` since the examples moved
|
|
464
|
+
// to upstream's spelling (2026-10-02); its ja is the current render.
|
|
407
465
|
const cases: ReadonlyArray<[string, string, string, string]> = [
|
|
408
466
|
[
|
|
409
467
|
'append-content',
|
|
@@ -413,8 +471,8 @@ describe('R2 execution validator (lock)', () => {
|
|
|
413
471
|
],
|
|
414
472
|
[
|
|
415
473
|
'increment-by-amount',
|
|
416
|
-
|
|
417
|
-
'#score を
|
|
474
|
+
"on click increment #score's textContent by 10",
|
|
475
|
+
'クリック を で #scoreのtextContent を 10 増加',
|
|
418
476
|
'text[10]',
|
|
419
477
|
],
|
|
420
478
|
];
|