@ohos-ports/floss 5.0.1-beta.1 → 5.0.1-beta.2

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.
@@ -0,0 +1,481 @@
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.failures > 0) {
464
+ reject(new Error(
465
+ "Mocha tests failed: " + result.failures +
466
+ " failure(s) out of " + (result.total || "unknown") + " test(s)."
467
+ ));
468
+ } else {
469
+ resolve();
470
+ }
471
+
472
+ } catch (e) {
473
+ // Cleanup on error
474
+ if (client) { try { client.close(); } catch (e2) {} }
475
+ if (browserLaunched) { try { await closeBrowser(); } catch (e3) {} }
476
+ reject(e);
477
+ }
478
+ });
479
+ }
480
+
481
+ exports.runInBrowser = runInBrowser;
package/lib/index.js CHANGED
@@ -48,14 +48,31 @@ 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). Use Node.js + jsdom runner.
52
- // This provides fully functional test execution using real Mocha and jsdom.
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)
53
57
  try {
54
- const nodeRunner = require('./node-runner');
55
- return nodeRunner.runInNode(opts);
58
+ const browserRunner = require('./browser-runner');
59
+ return browserRunner.runInBrowser(opts);
56
60
  }
57
- catch (e) {
58
- throw new Error('Unable to find Electron. Install \'electron\' alongside Floss. (Node.js runner failed: ' + e.message + ')');
61
+ catch (browserErr) {
62
+ // Level 2: Try node-runner (jsdom)
63
+ try {
64
+ const nodeRunner = require('./node-runner');
65
+ return nodeRunner.runInNode(opts);
66
+ }
67
+ catch (nodeErr) {
68
+ // Level 3: No runner available
69
+ throw new Error(
70
+ 'Unable to find Electron. No test runner available.\n' +
71
+ ' Browser runner (CDP) failed: ' + browserErr.message + '\n' +
72
+ ' Node runner (jsdom) failed: ' + nodeErr.message + '\n' +
73
+ 'Install \'electron\' alongside Floss, or ensure hdc + browser or jsdom is available.'
74
+ );
75
+ }
59
76
  }
60
77
  }
61
78
  const app = path.join(__dirname, 'main');
@@ -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.1",
3
+ "version": "5.0.1-beta.2",
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"