Files
openless/openless-all/app/scripts/android-accessibility-paste-cache-contract.test.mjs

94 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const servicePath = fileURLToPath(
new URL('../android/kotlin/OpenLessAccessibilityService.kt', import.meta.url),
);
const source = readFileSync(servicePath, 'utf8');
function kotlinFunctionBody(functionSignature) {
// Formatting may wrap parameters; keep the function name and signature checks.
const pattern = functionSignature
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/\s+/g, '\\s+')
.replaceAll('\\(', '\\(\\s*')
.replaceAll('\\)', '\\s*\\)');
const signatureIndex = source.search(new RegExp(pattern));
assert.notEqual(signatureIndex, -1, `missing Kotlin function: ${functionSignature}`);
const openBrace = source.indexOf('{', signatureIndex);
assert.notEqual(openBrace, -1, `missing opening brace: ${functionSignature}`);
let depth = 0;
for (let index = openBrace; index < source.length; index += 1) {
if (source[index] === '{') depth += 1;
if (source[index] === '}') depth -= 1;
if (depth === 0) return source.slice(openBrace + 1, index);
}
assert.fail(`missing closing brace: ${functionSignature}`);
}
const pasteBody = kotlinFunctionBody(
'private fun performPasteToFocusedFieldInternal(pasteText: String? = null)',
);
assert.match(
pasteBody,
/finally\s*\{\s*target\.recycle\(\)\s*\}/s,
'paste completion must recycle only the per-call target',
);
assert.doesNotMatch(
pasteBody,
/finally\s*\{[\s\S]*?invalidateEditableCache\s*\(/,
'paste completion must retain the validated focus cache for a consecutive paste',
);
const targetBody = kotlinFunctionBody('private fun findEditableTarget()');
const rootBody = kotlinFunctionBody('private fun findEditableInRoot(root: AccessibilityNodeInfo)');
assert.match(
targetBody,
/lastEditableFocus\?\.let\s*\{\s*cached\s*->[\s\S]*?OpenLessAccessibilityTarget\.isPasteTarget\(cached\)/s,
'findEditableTarget must try lenient cached paste target before window scans',
);
assert.match(
targetBody,
/for\s*\(\s*window\s+in\s+windows\s*\)/,
'findEditableTarget must scan all accessibility windows',
);
assert.match(
rootBody,
/editableFocusedNode\(root, AccessibilityNodeInfo\.FOCUS_INPUT\)\?\.let\s*\{\s*fresh\s*->\s*cacheEditableTarget\(fresh\)\s*return fresh/s,
'a fresh focus target must refresh the service cache',
);
assert.match(
rootBody,
/lastEditableFocus\?\.let\s*\{\s*cached\s*->[\s\S]*?OpenLessAccessibilityTarget\.isValidCachedEditable\(cached, root\)[\s\S]*?return AccessibilityNodeInfo\.obtain\(cached\)/s,
'cached focus reuse must retain package, window, focus, and refresh validation',
);
assert.match(
rootBody,
/editableFocusedNode\(root, AccessibilityNodeInfo\.FOCUS_ACCESSIBILITY\)/,
'findEditableInRoot must try accessibility focus after input focus',
);
assert.match(
rootBody,
/findEditableInTree\(root, 0\)/,
'findEditableInRoot must fall back to editable tree search',
);
assert.match(source, /pasteAppearsApplied/, 'paste must verify editor text changed');
assert.match(source, /paste=unverified/, 'paste must log unverified ACTION_PASTE results');
assert.match(
readFileSync(
fileURLToPath(
new URL('../android/kotlin/OpenLessAccessibilityCommandReceiver.kt', import.meta.url),
),
'utf8',
),
/EXTRA_PASTE_TEXT/,
'paste IPC must carry text to the accessibility process',
);
assert.match(source, /EXTRA_PASTE_TEXT/, 'paste sender must include IPC text extra');
console.log('Android accessibility paste cache contract checks passed');