dsh-apk-vm 0.6.2 → 0.8.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 +54 -0
- package/README.md +8 -3
- package/client.js +884 -16
- package/index.js +202 -7
- package/lib/aab.js +736 -0
- package/lib/android.js +95 -1
- package/lib/har.js +423 -0
- package/lib/ui-tests.js +393 -0
- package/package.json +6 -3
package/lib/ui-tests.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight UI assertions and visual baselines for the APK VM.
|
|
3
|
+
*
|
|
4
|
+
* `runAssertions` checks a UIAutomator dump (raw XML or already-parsed nodes)
|
|
5
|
+
* without touching a device. `compareScreenshot` fingerprints a PNG with SHA-256
|
|
6
|
+
* and stores that fingerprint next to the baseline; it is an exact-byte check,
|
|
7
|
+
* NOT a pixel diff, so a re-encoded but visually identical PNG counts as
|
|
8
|
+
* changed. Capture screenshots through one fixed pipeline (size, density,
|
|
9
|
+
* encoder) and use the result as a regression fingerprint.
|
|
10
|
+
*
|
|
11
|
+
* Baselines live in a caller-provided directory as
|
|
12
|
+
* `<safe-name>.png.sha256` (plain lowercase hash + newline); the PNG itself is
|
|
13
|
+
* saved on the first run and on `update: true`. Everything here is
|
|
14
|
+
* dependency-free and keeps no state outside those files.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { promises as fsp } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { parseUiHierarchy } from '../lib/ui-inspector.js';
|
|
21
|
+
|
|
22
|
+
const FIELDS = ['text', 'resourceId', 'contentDesc', 'className'];
|
|
23
|
+
const MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
|
|
24
|
+
const HASH_PATTERN = /^[0-9a-f]{64}$/i;
|
|
25
|
+
const HASH_SUFFIX = '.png.sha256';
|
|
26
|
+
const PNG_SUFFIX = '.png';
|
|
27
|
+
const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
28
|
+
const MAX_MARKDOWN_FAILURES = 10;
|
|
29
|
+
|
|
30
|
+
function normalizeText(value) {
|
|
31
|
+
return String(value == null ? '' : value)
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function folded(value) {
|
|
37
|
+
return normalizeText(value).toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shorten(value, limit = 80) {
|
|
41
|
+
const text = normalizeText(value);
|
|
42
|
+
return text.length > limit ? `${text.slice(0, Math.max(0, limit - 3))}...` : text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeNodes(input) {
|
|
46
|
+
let source = input;
|
|
47
|
+
if (typeof source === 'string') {
|
|
48
|
+
source = parseUiHierarchy(source).nodes;
|
|
49
|
+
} else if (source && !Array.isArray(source) && typeof source === 'object' && Array.isArray(source.nodes)) {
|
|
50
|
+
source = source.nodes;
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(source)) return [];
|
|
53
|
+
return source.map((node) => {
|
|
54
|
+
const record = node && typeof node === 'object' ? node : {};
|
|
55
|
+
const normalized = {};
|
|
56
|
+
for (const field of FIELDS) normalized[field] = folded(record[field]);
|
|
57
|
+
return normalized;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertionCriteria(assertion) {
|
|
62
|
+
const criteria = [];
|
|
63
|
+
for (const field of FIELDS) {
|
|
64
|
+
if (assertion[field] == null) continue;
|
|
65
|
+
const needle = folded(assertion[field]);
|
|
66
|
+
if (!needle) continue;
|
|
67
|
+
criteria.push({
|
|
68
|
+
field,
|
|
69
|
+
needle,
|
|
70
|
+
label: `${field}=${JSON.stringify(shorten(assertion[field], 60))}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return criteria;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function evaluateAssertion(nodes, assertion) {
|
|
77
|
+
if (!assertion || typeof assertion !== 'object' || Array.isArray(assertion)) {
|
|
78
|
+
return { ok: false, reason: 'assertion must be an object' };
|
|
79
|
+
}
|
|
80
|
+
const criteria = assertionCriteria(assertion);
|
|
81
|
+
if (criteria.length === 0) {
|
|
82
|
+
return { ok: false, reason: 'assertion has no selector field (text, resourceId, contentDesc or className)' };
|
|
83
|
+
}
|
|
84
|
+
const selector = criteria.map((criterion) => criterion.label).join(', ');
|
|
85
|
+
const matches = nodes.filter((node) =>
|
|
86
|
+
criteria.every((criterion) => node[criterion.field].includes(criterion.needle)),
|
|
87
|
+
);
|
|
88
|
+
const wantsMatch = assertion.exists !== false;
|
|
89
|
+
if (wantsMatch) {
|
|
90
|
+
return matches.length > 0
|
|
91
|
+
? { ok: true }
|
|
92
|
+
: { ok: false, reason: `no matching UI node for ${selector}` };
|
|
93
|
+
}
|
|
94
|
+
return matches.length === 0
|
|
95
|
+
? { ok: true }
|
|
96
|
+
: { ok: false, reason: `expected no matching UI node for ${selector}, found ${matches.length}` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run a list of `{ text?, resourceId?, contentDesc?, className?, exists? }`
|
|
101
|
+
* assertions against UI nodes. Matching is case-insensitive substring on
|
|
102
|
+
* trimmed/whitespace-collapsed fields; every provided field must match and
|
|
103
|
+
* `exists` defaults to `true`. `nodes` may be an array of parsed nodes, the
|
|
104
|
+
* `{ nodes }` object returned by parseUiHierarchy, or a raw UIAutomator XML
|
|
105
|
+
* string.
|
|
106
|
+
*/
|
|
107
|
+
export function runAssertions(nodes, assertions) {
|
|
108
|
+
const normalized = normalizeNodes(nodes);
|
|
109
|
+
const list = assertions == null ? [] : assertions;
|
|
110
|
+
if (!Array.isArray(list)) throw new TypeError('runAssertions requires an array of assertions');
|
|
111
|
+
const failed = [];
|
|
112
|
+
let passed = 0;
|
|
113
|
+
for (const assertion of list) {
|
|
114
|
+
const outcome = evaluateAssertion(normalized, assertion);
|
|
115
|
+
if (outcome.ok) {
|
|
116
|
+
passed += 1;
|
|
117
|
+
} else {
|
|
118
|
+
failed.push({ assertion, reason: outcome.reason });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ok: failed.length === 0, total: list.length, passed, failed };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function safeBaselineName(value) {
|
|
125
|
+
let name = String(value == null ? '' : value)
|
|
126
|
+
.trim()
|
|
127
|
+
.replace(/\.png$/i, '')
|
|
128
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
129
|
+
.replace(/\.{2,}/g, '.')
|
|
130
|
+
.replace(/^[.-]+|[.-]+$/g, '')
|
|
131
|
+
.replace(/\.png$/i, '')
|
|
132
|
+
.slice(0, 80);
|
|
133
|
+
if (!name) name = 'screenshot';
|
|
134
|
+
// Windows treats CON.txt like CON, so test the stem before the first dot.
|
|
135
|
+
if (WINDOWS_RESERVED_NAME.test(name.split('.')[0])) name = `_${name}`;
|
|
136
|
+
return name;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function screenshotBytes(buffer) {
|
|
140
|
+
let data;
|
|
141
|
+
if (Buffer.isBuffer(buffer)) {
|
|
142
|
+
data = buffer;
|
|
143
|
+
} else if (buffer instanceof Uint8Array) {
|
|
144
|
+
data = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
145
|
+
} else if (buffer instanceof ArrayBuffer) {
|
|
146
|
+
data = Buffer.from(buffer);
|
|
147
|
+
} else {
|
|
148
|
+
throw new TypeError('compareScreenshot requires a Buffer');
|
|
149
|
+
}
|
|
150
|
+
if (data.length === 0) throw new RangeError('screenshot buffer is empty');
|
|
151
|
+
if (data.length > MAX_SCREENSHOT_BYTES) {
|
|
152
|
+
throw new RangeError(`screenshot buffer exceeds the ${MAX_SCREENSHOT_BYTES / (1024 * 1024)} MB limit`);
|
|
153
|
+
}
|
|
154
|
+
return data;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function readBaselineHash(baselinePath) {
|
|
158
|
+
let raw;
|
|
159
|
+
try {
|
|
160
|
+
raw = await fsp.readFile(baselinePath, 'utf8');
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error && error.code === 'ENOENT') return null;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
// Accept our plain hash format and the `sha256sum` "<hash> <file>" form.
|
|
166
|
+
const token = raw.replace(/^\uFEFF/, '').trim().split(/\s+/)[0] || '';
|
|
167
|
+
if (!HASH_PATTERN.test(token)) {
|
|
168
|
+
throw new Error(`invalid screenshot baseline hash in ${baselinePath}`);
|
|
169
|
+
}
|
|
170
|
+
return token.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function saveBaseline(pngPath, baselinePath, data, hash) {
|
|
174
|
+
await fsp.writeFile(pngPath, data);
|
|
175
|
+
await fsp.writeFile(baselinePath, `${hash}\n`, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Compare a screenshot with `baselineDir/<safe-name>.png.sha256` using SHA-256
|
|
180
|
+
* over the exact bytes (not a pixel diff). The first call creates the baseline
|
|
181
|
+
* (and saves the PNG); `update: true` refreshes an existing baseline (and the
|
|
182
|
+
* PNG). Rejects buffers over 10 MB. Returns `{ ok, created, updated, changed,
|
|
183
|
+
* baselineHash, actualHash, baselinePath }`, where `baselineHash` is the hash
|
|
184
|
+
* stored after the call, `changed` compares against the baseline that existed
|
|
185
|
+
* before it, and `updated` records an explicit refresh request.
|
|
186
|
+
*/
|
|
187
|
+
export async function compareScreenshot(buffer, baselineDir, name, options = {}) {
|
|
188
|
+
const data = screenshotBytes(buffer);
|
|
189
|
+
if (typeof baselineDir !== 'string' || !baselineDir.trim()) {
|
|
190
|
+
throw new TypeError('compareScreenshot requires a baseline directory');
|
|
191
|
+
}
|
|
192
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
193
|
+
const update = opts.update === true;
|
|
194
|
+
const root = path.resolve(baselineDir);
|
|
195
|
+
const safeName = safeBaselineName(name);
|
|
196
|
+
const baselinePath = path.join(root, `${safeName}${HASH_SUFFIX}`);
|
|
197
|
+
const pngPath = path.join(root, `${safeName}${PNG_SUFFIX}`);
|
|
198
|
+
const actualHash = createHash('sha256').update(data).digest('hex');
|
|
199
|
+
|
|
200
|
+
await fsp.mkdir(root, { recursive: true });
|
|
201
|
+
const existingHash = await readBaselineHash(baselinePath);
|
|
202
|
+
const existed = existingHash != null;
|
|
203
|
+
const changed = existed && existingHash !== actualHash;
|
|
204
|
+
|
|
205
|
+
let created = false;
|
|
206
|
+
let updated = false;
|
|
207
|
+
let ok = true;
|
|
208
|
+
let baselineHash = existingHash;
|
|
209
|
+
if (!existed) {
|
|
210
|
+
await saveBaseline(pngPath, baselinePath, data, actualHash);
|
|
211
|
+
created = true;
|
|
212
|
+
baselineHash = actualHash;
|
|
213
|
+
} else if (update) {
|
|
214
|
+
await saveBaseline(pngPath, baselinePath, data, actualHash);
|
|
215
|
+
updated = true;
|
|
216
|
+
baselineHash = actualHash;
|
|
217
|
+
} else {
|
|
218
|
+
ok = !changed;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { ok, created, updated, changed, baselineHash, actualHash, baselinePath };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function asCount(value, fallback) {
|
|
225
|
+
if (value == null) return fallback;
|
|
226
|
+
const number = Number(value);
|
|
227
|
+
if (!Number.isFinite(number) || number < 0) return fallback;
|
|
228
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Math.round(number));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function sanitizeAssertion(assertion) {
|
|
232
|
+
if (!assertion || typeof assertion !== 'object' || Array.isArray(assertion)) return {};
|
|
233
|
+
const safe = {};
|
|
234
|
+
for (const field of FIELDS) {
|
|
235
|
+
if (assertion[field] != null && normalizeText(assertion[field])) {
|
|
236
|
+
safe[field] = shorten(assertion[field], 200);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (typeof assertion.exists === 'boolean') safe.exists = assertion.exists;
|
|
240
|
+
return safe;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeFailure(entry) {
|
|
244
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
245
|
+
return { assertion: {}, reason: shorten(entry, 500) || 'assertion failed' };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
assertion: sanitizeAssertion(entry.assertion),
|
|
249
|
+
reason: shorten(entry.reason, 500) || 'assertion failed',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Normalize a runAssertions result (or a plain passed/failed summary). */
|
|
254
|
+
function normalizeAssertions(input) {
|
|
255
|
+
const source = input && typeof input === 'object' && !Array.isArray(input) ? input : null;
|
|
256
|
+
if (!source) return { ok: true, total: 0, passed: 0, failed: 0, failures: [] };
|
|
257
|
+
const failureSource = Array.isArray(source.failed)
|
|
258
|
+
? source.failed
|
|
259
|
+
: Array.isArray(source.failures)
|
|
260
|
+
? source.failures
|
|
261
|
+
: [];
|
|
262
|
+
const failures = failureSource.map(normalizeFailure);
|
|
263
|
+
const failed = Array.isArray(source.failed) ? source.failed.length : asCount(source.failed, failures.length);
|
|
264
|
+
let passed = asCount(source.passed, null);
|
|
265
|
+
let total = asCount(source.total, null);
|
|
266
|
+
if (total == null) total = (passed == null ? 0 : passed) + failed;
|
|
267
|
+
if (passed == null) passed = Math.max(0, total - failed);
|
|
268
|
+
total = Math.max(total, passed + failed);
|
|
269
|
+
const ok = typeof source.ok === 'boolean' ? source.ok : failed === 0 && failures.length === 0;
|
|
270
|
+
return { ok, total, passed, failed, failures };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeVisual(input) {
|
|
274
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
275
|
+
const hash = (value) => (typeof value === 'string' && HASH_PATTERN.test(value) ? value.toLowerCase() : null);
|
|
276
|
+
const changed = Boolean(input.changed);
|
|
277
|
+
return {
|
|
278
|
+
ok: typeof input.ok === 'boolean' ? input.ok : !changed,
|
|
279
|
+
created: Boolean(input.created),
|
|
280
|
+
updated: Boolean(input.updated),
|
|
281
|
+
changed,
|
|
282
|
+
baselineHash: hash(input.baselineHash),
|
|
283
|
+
actualHash: hash(input.actualHash),
|
|
284
|
+
baselinePath: typeof input.baselinePath === 'string' ? input.baselinePath : null,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function escapeMarkdown(value) {
|
|
289
|
+
return String(value == null ? '' : value)
|
|
290
|
+
.replace(/\s+/g, ' ')
|
|
291
|
+
.trim()
|
|
292
|
+
.replace(/[\\`*_<>]/g, '\\$&');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function assertionLabel(assertion) {
|
|
296
|
+
const safe = sanitizeAssertion(assertion);
|
|
297
|
+
const parts = [];
|
|
298
|
+
for (const field of FIELDS) {
|
|
299
|
+
if (safe[field]) parts.push(`${field}=${JSON.stringify(shorten(safe[field], 60))}`);
|
|
300
|
+
}
|
|
301
|
+
if (safe.exists === false) parts.push('exists=false');
|
|
302
|
+
if (parts.length === 0) return '`(no selector)`';
|
|
303
|
+
return `\`${parts.join(', ').replace(/`/g, "'")}\``;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Build a JSON-serializable test report plus its compact Markdown rendering.
|
|
308
|
+
* `assertions` is normally the runAssertions result; `visual` is normally the
|
|
309
|
+
* compareScreenshot result. The report shape is `{ name, instanceId,
|
|
310
|
+
* generatedAt, durationMs, ok, assertions: { ok, total, passed, failed,
|
|
311
|
+
* failures }, visual }`. Returns `{ report, markdown }`.
|
|
312
|
+
*/
|
|
313
|
+
export function createTestReport(params = {}) {
|
|
314
|
+
const input = params && typeof params === 'object' ? params : {};
|
|
315
|
+
const assertions = normalizeAssertions(input.assertions);
|
|
316
|
+
const visual = normalizeVisual(input.visual);
|
|
317
|
+
const report = {
|
|
318
|
+
name: shorten(input.name, 200) || 'ui-test',
|
|
319
|
+
instanceId: shorten(input.instanceId, 120) || 'default',
|
|
320
|
+
generatedAt: new Date().toISOString(),
|
|
321
|
+
durationMs: asCount(input.durationMs, 0),
|
|
322
|
+
ok: assertions.ok && (!visual || visual.ok),
|
|
323
|
+
assertions,
|
|
324
|
+
visual,
|
|
325
|
+
};
|
|
326
|
+
return { report, markdown: reportMarkdown(report) };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Render a report (or a `{ report, ... }` wrapper) as a compact Markdown string. */
|
|
330
|
+
export function reportMarkdown(report) {
|
|
331
|
+
const source =
|
|
332
|
+
report && typeof report === 'object' && report.report && typeof report.report === 'object'
|
|
333
|
+
? report.report
|
|
334
|
+
: report;
|
|
335
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
336
|
+
return '# UI test report\n\n_No report data._\n';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const assertions = normalizeAssertions(source.assertions);
|
|
340
|
+
const visual = source.visual && typeof source.visual === 'object' ? normalizeVisual(source.visual) : null;
|
|
341
|
+
const ok = typeof source.ok === 'boolean' ? source.ok : assertions.ok && (!visual || visual.ok);
|
|
342
|
+
const lines = [];
|
|
343
|
+
lines.push(`# UI test: ${escapeMarkdown(shorten(source.name, 200) || 'ui-test')}`);
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push(`- Status: **${ok ? 'PASS' : 'FAIL'}**`);
|
|
346
|
+
if (normalizeText(source.instanceId)) lines.push(`- Instance: \`${shorten(source.instanceId, 120)}\``);
|
|
347
|
+
lines.push(`- Duration: ${asCount(source.durationMs, 0)} ms`);
|
|
348
|
+
if (typeof source.generatedAt === 'string' && source.generatedAt) {
|
|
349
|
+
lines.push(`- Generated: ${escapeMarkdown(source.generatedAt)}`);
|
|
350
|
+
}
|
|
351
|
+
lines.push('');
|
|
352
|
+
lines.push('## Assertions');
|
|
353
|
+
lines.push(`- ${assertions.passed} passed, ${assertions.failed} failed (${assertions.total} total).`);
|
|
354
|
+
if (assertions.failures.length === 0) {
|
|
355
|
+
if (assertions.failed > 0) {
|
|
356
|
+
lines.push(`- ${assertions.failed} failure(s) reported without details.`);
|
|
357
|
+
} else {
|
|
358
|
+
lines.push(assertions.total > 0 ? '- All assertions passed.' : '- No assertions recorded.');
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const shownFailures = assertions.failures.slice(0, MAX_MARKDOWN_FAILURES);
|
|
362
|
+
for (const failure of shownFailures) {
|
|
363
|
+
const reason = failure.reason ? ` - ${escapeMarkdown(failure.reason)}` : '';
|
|
364
|
+
lines.push(`- FAIL ${assertionLabel(failure.assertion)}${reason}`);
|
|
365
|
+
}
|
|
366
|
+
if (assertions.failures.length > shownFailures.length) {
|
|
367
|
+
lines.push(`- ...and ${assertions.failures.length - shownFailures.length} more failure(s).`);
|
|
368
|
+
}
|
|
369
|
+
const withoutDetails = assertions.failed - assertions.failures.length;
|
|
370
|
+
if (withoutDetails > 0) lines.push(`- ...and ${withoutDetails} failure(s) without details.`);
|
|
371
|
+
}
|
|
372
|
+
lines.push('');
|
|
373
|
+
lines.push('## Visual');
|
|
374
|
+
if (!visual) {
|
|
375
|
+
lines.push('- Not compared.');
|
|
376
|
+
} else {
|
|
377
|
+
lines.push(`- Result: ${visual.ok ? 'match' : 'changed'} (exact-byte SHA-256, not a pixel diff).`);
|
|
378
|
+
const state = visual.created
|
|
379
|
+
? 'created'
|
|
380
|
+
: visual.updated
|
|
381
|
+
? 'updated'
|
|
382
|
+
: visual.changed || !visual.ok
|
|
383
|
+
? 'changed'
|
|
384
|
+
: 'unchanged';
|
|
385
|
+
lines.push(`- Baseline: ${state}.`);
|
|
386
|
+
if (visual.baselineHash) lines.push(`- Baseline SHA-256: \`${visual.baselineHash}\``);
|
|
387
|
+
if (visual.actualHash && visual.actualHash !== visual.baselineHash) {
|
|
388
|
+
lines.push(`- Actual SHA-256: \`${visual.actualHash}\``);
|
|
389
|
+
}
|
|
390
|
+
if (visual.baselinePath) lines.push(`- Path: \`${visual.baselinePath.replace(/`/g, "'")}\``);
|
|
391
|
+
}
|
|
392
|
+
return `${lines.join('\n')}\n`;
|
|
393
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-apk-vm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local AOSP Android VM for DeepSeek Harness: preview rooted APKs at up to 60 fps, automate UIAutomator flows, and give each Agent an isolated VM.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"registry": "https://registry.npmjs.org/"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"check": "node --check index.js && node --check client.js && node --check lib/android.js && node --check lib/apk-bundle.js && node --check lib/flows.js && node --check lib/storage.js && node --check lib/ui-inspector.js && node --check lib/x509.js && node --check lib/scrcpy.js && node --check lib/ws.js && node --check lib/setup.js && node --check lib/instances.js && node --check lib/video-stream.js",
|
|
28
|
+
"check": "node --check index.js && node --check client.js && node --check lib/android.js && node --check lib/apk-bundle.js && node --check lib/flows.js && node --check lib/storage.js && node --check lib/ui-inspector.js && node --check lib/x509.js && node --check lib/scrcpy.js && node --check lib/ws.js && node --check lib/setup.js && node --check lib/instances.js && node --check lib/video-stream.js && node --check lib/aab.js && node --check lib/har.js && node --check lib/ui-tests.js",
|
|
29
29
|
"privacy": "node scripts/privacy-scan.mjs",
|
|
30
|
-
"test": "node spike/ui-smoke.cjs && node spike/ui-better-sidebar-smoke.cjs && node spike/setup-smoke.mjs && node spike/android/mock-device.mjs && node spike/android/launch-plan.mjs && node spike/instances-smoke.mjs && node spike/features.mjs"
|
|
30
|
+
"test": "node spike/ui-smoke.cjs && node spike/ui-better-sidebar-smoke.cjs && node spike/setup-smoke.mjs && node spike/android/mock-device.mjs && node spike/android/launch-plan.mjs && node spike/instances-smoke.mjs && node spike/features.mjs && node spike/aab-test.mjs && node spike/har-test.mjs && node spike/ui-tests-test.mjs"
|
|
31
31
|
},
|
|
32
32
|
"exports": {
|
|
33
33
|
".": "./index.js",
|
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
"lib/instances.js",
|
|
52
52
|
"lib/video-stream.js",
|
|
53
53
|
"lib/x509.js",
|
|
54
|
+
"lib/aab.js",
|
|
55
|
+
"lib/har.js",
|
|
56
|
+
"lib/ui-tests.js",
|
|
54
57
|
"lib/vendor/scrcpy-server-v5.0.1.jar",
|
|
55
58
|
"LICENSE",
|
|
56
59
|
"NOTICE",
|