@ohos-ports/floss 5.0.1-beta.1 → 5.0.1-beta.3
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/lib/browser-runner.js +486 -0
- package/lib/index.js +16 -5
- package/lib/node-runner.js +0 -7
- package/package.json +5 -2
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "____esModule", { value: true });
|
|
3
|
+
exports.runInBrowser = void 0;
|
|
4
|
+
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
const glob = require("glob");
|
|
9
|
+
|
|
10
|
+
// ─── Constants ─────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const BROWSER_PKG = "com.haitai.htbrowser";
|
|
13
|
+
const BROWSER_ABILITY = "EntryAbility";
|
|
14
|
+
const CDP_PORT = 9333;
|
|
15
|
+
|
|
16
|
+
const BOOL_FLAGS = [
|
|
17
|
+
"disable-extensions", "disable-default-apps",
|
|
18
|
+
"disable-component-extensions-with-background-pages",
|
|
19
|
+
"no-first-run", "no-default-browser-check",
|
|
20
|
+
"disable-background-networking",
|
|
21
|
+
"disable-client-side-phishing-detection", "disable-popup-blocking",
|
|
22
|
+
"disable-prompt-on-repost", "disable-breakpad",
|
|
23
|
+
"disable-hang-monitor", "disable-ipc-flooding-protection",
|
|
24
|
+
"metrics-recording-only", "disable-sync",
|
|
25
|
+
"disable-search-engine-choice-screen",
|
|
26
|
+
"disable-background-timer-throttling",
|
|
27
|
+
"disable-renderer-backgrounding",
|
|
28
|
+
"disable-backgrounding-occluded-windows",
|
|
29
|
+
"disable-field-trial-config", "disable-component-update",
|
|
30
|
+
"disable-dev-shm-usage", "no-service-autorun",
|
|
31
|
+
"disable-infobars", "allow-file-access-from-files",
|
|
32
|
+
"enable-automation", "allow-pre-commit-input",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const DISABLED_FEATURES = [
|
|
36
|
+
"AvoidUnnecessaryBeforeUnloadCheckSync",
|
|
37
|
+
"DestroyProfileOnBrowserClose",
|
|
38
|
+
"DialMediaRouteProvider", "GlobalMediaControls",
|
|
39
|
+
"HttpsUpgrades", "LensOverlay", "MediaRouter",
|
|
40
|
+
"PaintHolding", "Translate",
|
|
41
|
+
].join(",");
|
|
42
|
+
|
|
43
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
44
|
+
|
|
45
|
+
// ─── HDC utilities ────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function getHdcPath() {
|
|
48
|
+
if (process.platform === "openharmony") {
|
|
49
|
+
try {
|
|
50
|
+
fs.accessSync("/data/service/hnp/bin/hdc");
|
|
51
|
+
return "/data/service/hnp/bin/hdc";
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
return process.env.HDC_PATH || "hdc";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function execHdc(args, timeout) {
|
|
58
|
+
const { execFileSync } = require("child_process");
|
|
59
|
+
try {
|
|
60
|
+
return execFileSync(getHdcPath(), args, {
|
|
61
|
+
timeout: timeout || 30000,
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
+
}).trim();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error("hdc " + args.join(" ") + " failed: " + e.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Browser launch / close ───────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function buildLaunchArgs() {
|
|
73
|
+
const args = [
|
|
74
|
+
"--remote-debugging-port=" + CDP_PORT,
|
|
75
|
+
"--remote-allow-origins=http://127.0.0.1:" + CDP_PORT,
|
|
76
|
+
];
|
|
77
|
+
for (var i = 0; i < BOOL_FLAGS.length; i++) {
|
|
78
|
+
args.push("--" + BOOL_FLAGS[i]);
|
|
79
|
+
}
|
|
80
|
+
args.push("--password-store=basic", "--use-mock-keychain");
|
|
81
|
+
args.push("--force-color-profile=srgb");
|
|
82
|
+
args.push("--disable-features=" + DISABLED_FEATURES);
|
|
83
|
+
return args.join(" ");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function launchBrowser() {
|
|
87
|
+
// 1. Check if browser is installed
|
|
88
|
+
var installedApps;
|
|
89
|
+
try {
|
|
90
|
+
installedApps = execHdc(["shell", "bm", "dump", "-a"], 10000);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new Error("Cannot query installed apps via hdc. Is hdc available? " + e.message);
|
|
93
|
+
}
|
|
94
|
+
if (!installedApps.includes(BROWSER_PKG)) {
|
|
95
|
+
throw new Error('Browser "' + BROWSER_PKG + '" is not installed on this device.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Kill existing browser process
|
|
99
|
+
try { execHdc(["shell", "aa", "force-stop", BROWSER_PKG], 10000); } catch {}
|
|
100
|
+
await sleep(1000);
|
|
101
|
+
|
|
102
|
+
// 3. Start browser with CDP enabled
|
|
103
|
+
var cmdArgs = buildLaunchArgs();
|
|
104
|
+
execHdc([
|
|
105
|
+
"shell", "aa", "start",
|
|
106
|
+
"-b", BROWSER_PKG,
|
|
107
|
+
"-a", BROWSER_ABILITY,
|
|
108
|
+
"--ps", "cmdArgs", cmdArgs,
|
|
109
|
+
], 15000);
|
|
110
|
+
|
|
111
|
+
// 4. Wait for CDP endpoint
|
|
112
|
+
var wsUrl = await waitForCdpEndpoint(CDP_PORT, 30000);
|
|
113
|
+
return wsUrl;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function closeBrowser() {
|
|
117
|
+
try { execHdc(["shell", "aa", "force-stop", BROWSER_PKG], 10000); } catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── CDP endpoint discovery ──────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function httpGet(url, timeout) {
|
|
123
|
+
return new Promise(function (resolve, reject) {
|
|
124
|
+
var req = http.get(url, function (res) {
|
|
125
|
+
var data = "";
|
|
126
|
+
res.on("data", function (chunk) { data += chunk; });
|
|
127
|
+
res.on("end", function () {
|
|
128
|
+
if (res.statusCode !== 200) return reject(new Error("HTTP " + res.statusCode));
|
|
129
|
+
try { resolve(JSON.parse(data)); }
|
|
130
|
+
catch (e) { reject(new Error("JSON parse failed: " + e.message)); }
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
req.on("error", reject);
|
|
134
|
+
req.setTimeout(timeout || 2000, function () { req.destroy(); reject(new Error("Timeout")); });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function waitForCdpEndpoint(port, timeoutMs) {
|
|
139
|
+
var start = Date.now();
|
|
140
|
+
while (Date.now() - start < timeoutMs) {
|
|
141
|
+
try {
|
|
142
|
+
var data = await httpGet("http://127.0.0.1:" + port + "/json/version", 2000);
|
|
143
|
+
if (data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
try {
|
|
146
|
+
var data6 = await httpGet("http://[::1]:" + port + "/json/version", 2000);
|
|
147
|
+
if (data6.webSocketDebuggerUrl) return data6.webSocketDebuggerUrl;
|
|
148
|
+
} catch (e2) {
|
|
149
|
+
await sleep(500);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw new Error("CDP endpoint timeout on port " + port + " after " + timeoutMs + "ms. " +
|
|
154
|
+
"Ensure browser is running and CDP is enabled.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── CDP WebSocket client ────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function createWebSocket(url) {
|
|
160
|
+
// Try 'ws' package first (most reliable)
|
|
161
|
+
var WSImpl = null;
|
|
162
|
+
try { WSImpl = require("ws"); }
|
|
163
|
+
catch (e) {
|
|
164
|
+
// Fall back to built-in WebSocket (Node.js 22+)
|
|
165
|
+
if (typeof globalThis.WebSocket !== "undefined") {
|
|
166
|
+
WSImpl = globalThis.WebSocket;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!WSImpl) {
|
|
170
|
+
throw new Error("No WebSocket available. Install 'ws' package or use Node.js 22+.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
var ws = new WSImpl(url);
|
|
174
|
+
|
|
175
|
+
// Detect ws package (EventEmitter style) vs built-in (addEventListener style)
|
|
176
|
+
var isWsPkg = typeof ws.on === "function";
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
onOpen: function (cb) {
|
|
180
|
+
if (isWsPkg) ws.on("open", cb);
|
|
181
|
+
else ws.addEventListener("open", cb);
|
|
182
|
+
},
|
|
183
|
+
onMessage: function (cb) {
|
|
184
|
+
if (isWsPkg) {
|
|
185
|
+
ws.on("message", function (data) {
|
|
186
|
+
cb(typeof data === "string" ? data : data.toString());
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
ws.addEventListener("message", function (e) { cb(e.data); });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
onError: function (cb) {
|
|
193
|
+
if (isWsPkg) ws.on("error", cb);
|
|
194
|
+
else ws.addEventListener("error", function (e) { cb(e); });
|
|
195
|
+
},
|
|
196
|
+
send: function (data) { ws.send(data); },
|
|
197
|
+
close: function () { try { ws.close(); } catch (e) {} },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function CdpClient(wsUrl) {
|
|
202
|
+
this.wsUrl = wsUrl;
|
|
203
|
+
this.id = 0;
|
|
204
|
+
this.callbacks = {};
|
|
205
|
+
this.eventHandlers = {};
|
|
206
|
+
this.ws = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
CdpClient.prototype.connect = function () {
|
|
210
|
+
var self = this;
|
|
211
|
+
return new Promise(function (resolve, reject) {
|
|
212
|
+
self.ws = createWebSocket(self.wsUrl);
|
|
213
|
+
self.ws.onOpen(function () { resolve(); });
|
|
214
|
+
self.ws.onError(function (err) {
|
|
215
|
+
reject(new Error("WebSocket connection error: " + (err && err.message || err)));
|
|
216
|
+
});
|
|
217
|
+
self.ws.onMessage(function (data) {
|
|
218
|
+
var msg;
|
|
219
|
+
try { msg = JSON.parse(data); }
|
|
220
|
+
catch (e) { return; }
|
|
221
|
+
|
|
222
|
+
if (msg.id !== undefined) {
|
|
223
|
+
// Response to a command
|
|
224
|
+
var cb = self.callbacks[msg.id];
|
|
225
|
+
if (cb) {
|
|
226
|
+
delete self.callbacks[msg.id];
|
|
227
|
+
if (msg.error) {
|
|
228
|
+
cb.reject(new Error("CDP error: " + JSON.stringify(msg.error)));
|
|
229
|
+
} else {
|
|
230
|
+
cb.resolve(msg.result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (msg.method) {
|
|
234
|
+
// Event
|
|
235
|
+
var handlers = self.eventHandlers[msg.method] || [];
|
|
236
|
+
for (var i = 0; i < handlers.length; i++) {
|
|
237
|
+
handlers[i](msg.params || {}, msg.sessionId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
CdpClient.prototype.send = function (method, params, sessionId) {
|
|
245
|
+
var self = this;
|
|
246
|
+
var id = ++this.id;
|
|
247
|
+
var msg = { id: id, method: method, params: params || {} };
|
|
248
|
+
if (sessionId) msg.sessionId = sessionId;
|
|
249
|
+
|
|
250
|
+
return new Promise(function (resolve, reject) {
|
|
251
|
+
self.callbacks[id] = { resolve: resolve, reject: reject };
|
|
252
|
+
self.ws.send(JSON.stringify(msg));
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
CdpClient.prototype.on = function (method, handler) {
|
|
257
|
+
if (!this.eventHandlers[method]) this.eventHandlers[method] = [];
|
|
258
|
+
this.eventHandlers[method].push(handler);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
CdpClient.prototype.close = function () {
|
|
262
|
+
if (this.ws) this.ws.close();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// ─── File resolution (same logic as renderer.js addFile) ─────────────
|
|
266
|
+
|
|
267
|
+
function resolveFiles(files) {
|
|
268
|
+
return files
|
|
269
|
+
.map(function (file) {
|
|
270
|
+
var testPath = path.resolve(file);
|
|
271
|
+
if (!fs.existsSync(testPath)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (fs.statSync(testPath).isDirectory()) {
|
|
275
|
+
var indexFile = path.join(testPath, "index.js");
|
|
276
|
+
var indexTs = path.join(testPath, "index.ts");
|
|
277
|
+
if (fs.existsSync(indexFile)) {
|
|
278
|
+
return indexFile;
|
|
279
|
+
} else if (fs.existsSync(indexTs)) {
|
|
280
|
+
return indexTs;
|
|
281
|
+
}
|
|
282
|
+
console.error("No index.js file found in directory: " + testPath);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return testPath;
|
|
286
|
+
})
|
|
287
|
+
.filter(function (file) { return file !== null; });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Main: run tests in browser via CDP ──────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Run tests in a real Chromium browser (海泰浏览器) via CDP.
|
|
294
|
+
*
|
|
295
|
+
* This provides full browser capabilities: DOM, Canvas, WebGL, CSS layout.
|
|
296
|
+
* Browser is launched via hdc, controlled via Chrome DevTools Protocol.
|
|
297
|
+
*
|
|
298
|
+
* @param {Object} opts - Floss options (path, reporter, require, quiet, etc.)
|
|
299
|
+
* @returns {Promise<void>} Resolves on success, rejects on failure
|
|
300
|
+
*/
|
|
301
|
+
function runInBrowser(opts) {
|
|
302
|
+
return new Promise(async function (resolve, reject) {
|
|
303
|
+
var client = null;
|
|
304
|
+
var browserLaunched = false;
|
|
305
|
+
var sessionId = null;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// ── 1. Launch browser via hdc ──
|
|
309
|
+
var wsUrl = await launchBrowser();
|
|
310
|
+
browserLaunched = true;
|
|
311
|
+
|
|
312
|
+
// ── 2. Connect to CDP browser endpoint ──
|
|
313
|
+
client = new CdpClient(wsUrl);
|
|
314
|
+
await client.connect();
|
|
315
|
+
|
|
316
|
+
// ── 3. Create a new page (tab) ──
|
|
317
|
+
var targetResult = await client.send("Target.createTarget", {
|
|
318
|
+
url: "about:blank",
|
|
319
|
+
});
|
|
320
|
+
var targetId = targetResult.targetId;
|
|
321
|
+
|
|
322
|
+
// ── 4. Attach to the target (flatten mode for session-based commands) ──
|
|
323
|
+
var attachResult = await client.send("Target.attachToTarget", {
|
|
324
|
+
targetId: targetId,
|
|
325
|
+
flatten: true,
|
|
326
|
+
});
|
|
327
|
+
sessionId = attachResult.sessionId;
|
|
328
|
+
|
|
329
|
+
// ── 5. Enable Runtime + Page domains ──
|
|
330
|
+
await client.send("Runtime.enable", {}, sessionId);
|
|
331
|
+
await client.send("Page.enable", {}, sessionId);
|
|
332
|
+
|
|
333
|
+
// ── 6. Wait for page to be ready ──
|
|
334
|
+
await new Promise(function (pageResolve) {
|
|
335
|
+
var done = false;
|
|
336
|
+
var timeout = setTimeout(function () {
|
|
337
|
+
if (!done) { done = true; pageResolve(); }
|
|
338
|
+
}, 5000);
|
|
339
|
+
client.on("Page.loadEventFired", function (_params, sid) {
|
|
340
|
+
if (sid === sessionId && !done) {
|
|
341
|
+
done = true;
|
|
342
|
+
clearTimeout(timeout);
|
|
343
|
+
pageResolve();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ── 7. Set up test page HTML ──
|
|
349
|
+
await client.send("Runtime.evaluate", {
|
|
350
|
+
expression: 'document.body.innerHTML = ' +
|
|
351
|
+
'"<div id=\\"mocha\\"></div>' +
|
|
352
|
+
'<div id=\\"messages\\"></div>' +
|
|
353
|
+
'<div id=\\"fixtures\\"></div>";',
|
|
354
|
+
}, sessionId);
|
|
355
|
+
|
|
356
|
+
// ── 8. Set up result capture via console ──
|
|
357
|
+
var resultPromise = new Promise(function (resResolve, resReject) {
|
|
358
|
+
var settled = false;
|
|
359
|
+
var testTimeout = setTimeout(function () {
|
|
360
|
+
if (!settled) {
|
|
361
|
+
settled = true;
|
|
362
|
+
resReject(new Error("Test execution timeout (120s)"));
|
|
363
|
+
}
|
|
364
|
+
}, 120000);
|
|
365
|
+
|
|
366
|
+
client.on("Runtime.consoleAPICalled", function (params, sid) {
|
|
367
|
+
if (sid !== sessionId) return;
|
|
368
|
+
if (!params.args || params.args.length === 0) return;
|
|
369
|
+
|
|
370
|
+
var text = params.args.map(function (a) {
|
|
371
|
+
return a.value !== undefined ? a.value : (a.description || "");
|
|
372
|
+
}).join("");
|
|
373
|
+
|
|
374
|
+
// Forward console output to Node.js stdout (unless quiet)
|
|
375
|
+
if (!opts.quiet && !text.startsWith("__FLOSS_RESULT__:")) {
|
|
376
|
+
process.stdout.write(text + "\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Capture result marker
|
|
380
|
+
if (text.startsWith("__FLOSS_RESULT__:")) {
|
|
381
|
+
if (!settled) {
|
|
382
|
+
settled = true;
|
|
383
|
+
clearTimeout(testTimeout);
|
|
384
|
+
try {
|
|
385
|
+
var result = JSON.parse(
|
|
386
|
+
text.substring("__FLOSS_RESULT__:".length)
|
|
387
|
+
);
|
|
388
|
+
resResolve(result);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
resReject(new Error("Failed to parse test results: " + e.message));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ── 9. Inject Mocha browser bundle ──
|
|
398
|
+
var mochaPkgDir = path.dirname(require.resolve("mocha/package.json"));
|
|
399
|
+
var mochaBrowserPath = path.join(mochaPkgDir, "mocha.js");
|
|
400
|
+
if (!fs.existsSync(mochaBrowserPath)) {
|
|
401
|
+
throw new Error("Mocha browser bundle not found at: " + mochaBrowserPath);
|
|
402
|
+
}
|
|
403
|
+
var mochaContent = fs.readFileSync(mochaBrowserPath, "utf-8");
|
|
404
|
+
await client.send("Runtime.evaluate", {
|
|
405
|
+
expression: mochaContent,
|
|
406
|
+
}, sessionId);
|
|
407
|
+
|
|
408
|
+
// ── 10. Setup Mocha BDD interface ──
|
|
409
|
+
await client.send("Runtime.evaluate", {
|
|
410
|
+
expression: 'mocha.setup({ ui: "bdd" });',
|
|
411
|
+
}, sessionId);
|
|
412
|
+
|
|
413
|
+
// ── 11. Load additional module if specified ──
|
|
414
|
+
// Note: opts.require (e.g. ts-node/register) is Node.js-specific
|
|
415
|
+
// and cannot run in browser context. Skipped for CDP path.
|
|
416
|
+
if (opts.require) {
|
|
417
|
+
process.stderr.write(
|
|
418
|
+
"[floss] Warning: --require option is not supported in browser mode " +
|
|
419
|
+
"(CDP path). Module loading is Node.js-specific.\n"
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── 12. Inject test files ──
|
|
424
|
+
var files = glob.sync(opts.path);
|
|
425
|
+
var resolvedFiles = resolveFiles(files);
|
|
426
|
+
|
|
427
|
+
if (resolvedFiles.length === 0) {
|
|
428
|
+
throw new Error("No test files found for path: " + opts.path);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (var i = 0; i < resolvedFiles.length; i++) {
|
|
432
|
+
var fileContent = fs.readFileSync(resolvedFiles[i], "utf-8");
|
|
433
|
+
// Inject test file content directly.
|
|
434
|
+
// describe/it/before/after are global (set up by mocha.setup).
|
|
435
|
+
// Files using require()/module.exports will not work in browser context.
|
|
436
|
+
await client.send("Runtime.evaluate", {
|
|
437
|
+
expression: fileContent,
|
|
438
|
+
}, sessionId);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── 13. Run Mocha tests ──
|
|
442
|
+
await client.send("Runtime.evaluate", {
|
|
443
|
+
expression: [
|
|
444
|
+
"mocha.run(function(failures) {",
|
|
445
|
+
" var result = {",
|
|
446
|
+
" failures: failures,",
|
|
447
|
+
" total: (typeof mocha !== 'undefined' && mocha.stats) ? mocha.stats.total : 0,",
|
|
448
|
+
" passes: (typeof mocha !== 'undefined' && mocha.stats) ? mocha.stats.passes : 0",
|
|
449
|
+
" };",
|
|
450
|
+
" console.log('__FLOSS_RESULT__:' + JSON.stringify(result));",
|
|
451
|
+
"});",
|
|
452
|
+
].join("\n"),
|
|
453
|
+
}, sessionId);
|
|
454
|
+
|
|
455
|
+
// ── 14. Wait for test results ──
|
|
456
|
+
var result = await resultPromise;
|
|
457
|
+
|
|
458
|
+
// ── 15. Cleanup ──
|
|
459
|
+
client.close();
|
|
460
|
+
await closeBrowser();
|
|
461
|
+
|
|
462
|
+
// ── 16. Return result ──
|
|
463
|
+
if (result.total === 0) {
|
|
464
|
+
reject(new Error(
|
|
465
|
+
"No tests executed in browser. Test files likely use require() " +
|
|
466
|
+
"which is not available in browser context."
|
|
467
|
+
));
|
|
468
|
+
} else if (result.failures > 0) {
|
|
469
|
+
reject(new Error(
|
|
470
|
+
"Mocha tests failed: " + result.failures +
|
|
471
|
+
" failure(s) out of " + (result.total || "unknown") + " test(s)."
|
|
472
|
+
));
|
|
473
|
+
} else {
|
|
474
|
+
resolve();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
} catch (e) {
|
|
478
|
+
// Cleanup on error
|
|
479
|
+
if (client) { try { client.close(); } catch (e2) {} }
|
|
480
|
+
if (browserLaunched) { try { await closeBrowser(); } catch (e3) {} }
|
|
481
|
+
reject(e);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
exports.runInBrowser = runInBrowser;
|
package/lib/index.js
CHANGED
|
@@ -48,15 +48,26 @@ function floss(options) {
|
|
|
48
48
|
throw new Error('Path type is not a string');
|
|
49
49
|
}
|
|
50
50
|
if (!opts.electron) {
|
|
51
|
-
// Electron not available (e.g., HarmonyOS).
|
|
52
|
-
//
|
|
51
|
+
// Electron not available (e.g., HarmonyOS). Try three fallback levels:
|
|
52
|
+
// Level 1: 海泰浏览器 (Chromium) via CDP — full browser capabilities
|
|
53
|
+
// Level 2: jsdom — basic DOM simulation (pure JS, no rendering)
|
|
54
|
+
// Level 3: Error — no runner available
|
|
55
|
+
|
|
56
|
+
// Level 1: Try browser-runner (海泰浏览器 + CDP)
|
|
57
|
+
// Level 2: Fall back to node-runner (jsdom) on any failure
|
|
53
58
|
try {
|
|
59
|
+
const browserRunner = require('./browser-runner');
|
|
60
|
+
return browserRunner.runInBrowser(opts).catch((browserErr) => {
|
|
61
|
+
// Browser runner failed (launch error, 0 tests, or timeout)
|
|
62
|
+
const nodeRunner = require('./node-runner');
|
|
63
|
+
return nodeRunner.runInNode(opts);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (browserErr) {
|
|
67
|
+
// require('./browser-runner') failed synchronously
|
|
54
68
|
const nodeRunner = require('./node-runner');
|
|
55
69
|
return nodeRunner.runInNode(opts);
|
|
56
70
|
}
|
|
57
|
-
catch (e) {
|
|
58
|
-
throw new Error('Unable to find Electron. Install \'electron\' alongside Floss. (Node.js runner failed: ' + e.message + ')');
|
|
59
|
-
}
|
|
60
71
|
}
|
|
61
72
|
const app = path.join(__dirname, 'main');
|
|
62
73
|
const args = JSON.stringify(opts);
|
package/lib/node-runner.js
CHANGED
|
@@ -19,13 +19,6 @@ const querystring = require("querystring");
|
|
|
19
19
|
function runInNode(opts) {
|
|
20
20
|
return new Promise((resolve, reject) => {
|
|
21
21
|
try {
|
|
22
|
-
// Set global options (same as renderer.js line 41)
|
|
23
|
-
// This allows test code to access custom options via global.options
|
|
24
|
-
global.options = opts;
|
|
25
|
-
// Inject args into process.argv (same as Electron spawn behavior)
|
|
26
|
-
if (opts.args && opts.args.length) {
|
|
27
|
-
process.argv.push(...opts.args);
|
|
28
|
-
}
|
|
29
22
|
// Set up jsdom environment for DOM-dependent tests
|
|
30
23
|
// jsdom provides a real DOM implementation (not a stub)
|
|
31
24
|
const { JSDOM } = require('jsdom');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ohos-ports/floss",
|
|
3
|
-
"version": "5.0.1-beta.
|
|
3
|
+
"version": "5.0.1-beta.3",
|
|
4
4
|
"description": "Unit-testing for those hard to reach places",
|
|
5
5
|
"bin": "./lib/floss.js",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -64,10 +64,13 @@
|
|
|
64
64
|
"chalk": "^4.1.0",
|
|
65
65
|
"commander": "^7.0.0",
|
|
66
66
|
"glob": "^7.1.6",
|
|
67
|
-
"jsdom": "^30.0.1",
|
|
68
67
|
"mocha": "^8.0.0",
|
|
69
68
|
"resolve": "^1.19.0"
|
|
70
69
|
},
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"jsdom": "^30.0.1",
|
|
72
|
+
"ws": "^8.0.0"
|
|
73
|
+
},
|
|
71
74
|
"peerDependencies": {
|
|
72
75
|
"electron": ">=12 <14",
|
|
73
76
|
"nyc": ">=13"
|