@lvce-editor/test-with-playwright-worker 18.1.0 → 20.0.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.
Files changed (2) hide show
  1. package/dist/workerMain.js +377 -36
  2. package/package.json +3 -2
@@ -1,10 +1,11 @@
1
- import { basename, join } from 'node:path';
1
+ import { join, basename } from 'node:path';
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { expect, firefox, chromium } from '@playwright/test';
4
+ import { pathToFileURL } from 'node:url';
4
5
  import net from 'node:net';
5
6
  import os from 'node:os';
6
- import { pathToFileURL } from 'node:url';
7
- import { fork } from 'node:child_process';
7
+ import { fork, spawn } from 'node:child_process';
8
+ import { createInterface } from 'node:readline';
8
9
 
9
10
  const normalizeLine = line => {
10
11
  if (line.startsWith('Error: ')) {
@@ -98,6 +99,12 @@ const getType = value => {
98
99
  return Unknown;
99
100
  }
100
101
  };
102
+ const object = value => {
103
+ const type = getType(value);
104
+ if (type !== Object$1) {
105
+ throw new AssertionError('expected value to be of type object');
106
+ }
107
+ };
101
108
  const number = value => {
102
109
  const type = getType(value);
103
110
  if (type !== Number) {
@@ -904,33 +911,169 @@ const getTests = async testSrc => {
904
911
 
905
912
  const Cli = 6001;
906
913
 
907
- const Pass$1 = 'pass';
908
- const Skip$1 = 'skip';
909
- const Fail$1 = 'fail';
914
+ const Pass$1 = 1;
915
+ const Skip$1 = 2;
916
+ const Fail$1 = 3;
910
917
 
911
- const Pass = 1;
912
- const Skip = 2;
913
- const Fail = 3;
918
+ const importTestModule = async (testSrc, test) => {
919
+ const testPath = join(testSrc, test);
920
+ const testModule = await import(pathToFileURL(testPath).toString());
921
+ return testModule;
922
+ };
923
+ const withTimeout = async (promise, timeout) => {
924
+ return new Promise((resolve, reject) => {
925
+ const timer = setTimeout(() => {
926
+ reject(new Error(`Electron test timed out after ${timeout}ms`));
927
+ }, timeout);
928
+ void (async () => {
929
+ try {
930
+ const value = await promise;
931
+ resolve(value);
932
+ } catch (error) {
933
+ reject(error instanceof Error ? error : new Error(String(error)));
934
+ } finally {
935
+ clearTimeout(timer);
936
+ }
937
+ })();
938
+ });
939
+ };
940
+ const runElectronTest = async ({
941
+ electronApp,
942
+ page,
943
+ test,
944
+ testSrc,
945
+ timeout
946
+ }) => {
947
+ const start = performance.now();
948
+ try {
949
+ const testModule = await importTestModule(testSrc, test);
950
+ if (testModule.skip) {
951
+ const end = performance.now();
952
+ return {
953
+ end,
954
+ error: '',
955
+ name: test,
956
+ start,
957
+ status: Skip$1
958
+ };
959
+ }
960
+ await withTimeout(testModule.test({
961
+ electronApp,
962
+ expect,
963
+ Locator: page.locator.bind(page),
964
+ page
965
+ }), timeout);
966
+ const end = performance.now();
967
+ return {
968
+ end,
969
+ error: '',
970
+ name: test,
971
+ start,
972
+ status: Pass$1
973
+ };
974
+ } catch (error) {
975
+ const end = performance.now();
976
+ const message = error instanceof Error ? error.message : String(error);
977
+ return {
978
+ end,
979
+ error: message,
980
+ name: test,
981
+ start,
982
+ status: Fail$1
983
+ };
984
+ }
985
+ };
986
+
987
+ const getResultCounts$1 = status => {
988
+ switch (status) {
989
+ case Fail$1:
990
+ return {
991
+ failed: 1,
992
+ passed: 0,
993
+ skipped: 0
994
+ };
995
+ case Pass$1:
996
+ return {
997
+ failed: 0,
998
+ passed: 1,
999
+ skipped: 0
1000
+ };
1001
+ case Skip$1:
1002
+ return {
1003
+ failed: 0,
1004
+ passed: 0,
1005
+ skipped: 1
1006
+ };
1007
+ default:
1008
+ return {
1009
+ failed: 0,
1010
+ passed: 0,
1011
+ skipped: 0
1012
+ };
1013
+ }
1014
+ };
1015
+ const runElectronTests = async ({
1016
+ electronApp,
1017
+ filter,
1018
+ onFinalResult,
1019
+ onResult,
1020
+ page,
1021
+ tests,
1022
+ testSrc,
1023
+ timeout
1024
+ }) => {
1025
+ let failed = 0;
1026
+ let skipped = 0;
1027
+ let passed = 0;
1028
+ const start = performance.now();
1029
+ const filteredTests = filter ? tests.filter(test => test.includes(filter)) : tests;
1030
+ for (const test of filteredTests) {
1031
+ const result = await runElectronTest({
1032
+ electronApp,
1033
+ page,
1034
+ test,
1035
+ testSrc,
1036
+ timeout
1037
+ });
1038
+ await onResult(result);
1039
+ const resultCounts = getResultCounts$1(result.status);
1040
+ failed += resultCounts.failed;
1041
+ passed += resultCounts.passed;
1042
+ skipped += resultCounts.skipped;
1043
+ }
1044
+ const end = performance.now();
1045
+ await onFinalResult({
1046
+ end,
1047
+ failed,
1048
+ passed,
1049
+ skipped,
1050
+ start
1051
+ });
1052
+ };
1053
+
1054
+ const Pass = 'pass';
1055
+ const Skip = 'skip';
1056
+ const Fail = 'fail';
914
1057
 
915
1058
  const getTestState = (testOverlayState, text) => {
916
1059
  switch (testOverlayState) {
917
- case Fail$1:
1060
+ case Fail:
918
1061
  // @ts-ignore
919
1062
  return {
920
1063
  error: text,
921
- status: Fail
1064
+ status: Fail$1
922
1065
  };
923
- case Pass$1:
1066
+ case Pass:
924
1067
  // @ts-ignore
925
1068
  return {
926
1069
  error: '',
927
- status: Pass
1070
+ status: Pass$1
928
1071
  };
929
- case Skip$1:
1072
+ case Skip:
930
1073
  // @ts-ignore
931
1074
  return {
932
1075
  error: '',
933
- status: Skip
1076
+ status: Skip$1
934
1077
  };
935
1078
  default:
936
1079
  throw new Error(`unexpected test state: ${testOverlayState}`);
@@ -990,26 +1133,26 @@ const runTest = async ({
990
1133
  error: message,
991
1134
  name: test,
992
1135
  start,
993
- status: Fail
1136
+ status: Fail$1
994
1137
  };
995
1138
  }
996
1139
  };
997
1140
 
998
1141
  const getResultCounts = status => {
999
1142
  switch (status) {
1000
- case Fail:
1143
+ case Fail$1:
1001
1144
  return {
1002
1145
  failed: 1,
1003
1146
  passed: 0,
1004
1147
  skipped: 0
1005
1148
  };
1006
- case Pass:
1149
+ case Pass$1:
1007
1150
  return {
1008
1151
  failed: 0,
1009
1152
  passed: 1,
1010
1153
  skipped: 0
1011
1154
  };
1012
- case Skip:
1155
+ case Skip$1:
1013
1156
  return {
1014
1157
  failed: 0,
1015
1158
  passed: 0,
@@ -1195,7 +1338,6 @@ async function getPorts(options) {
1195
1338
  }
1196
1339
 
1197
1340
  const getPort = () => {
1198
- // @ts-ignore
1199
1341
  return getPorts();
1200
1342
  };
1201
1343
 
@@ -1325,14 +1467,194 @@ const setupTests = async ({
1325
1467
  };
1326
1468
  };
1327
1469
 
1470
+ const getProcessEnv = () => {
1471
+ const env = Object.create(null);
1472
+ for (const [key, value] of Object.entries(process.env)) {
1473
+ if (value !== undefined) {
1474
+ env[key] = value;
1475
+ }
1476
+ }
1477
+ return env;
1478
+ };
1479
+ const getElectronLaunchOptions = ({
1480
+ args,
1481
+ env,
1482
+ executablePath
1483
+ }) => {
1484
+ const launchEnv = {
1485
+ ...getProcessEnv(),
1486
+ ...env
1487
+ };
1488
+ delete launchEnv['ELECTRON_RUN_AS_NODE'];
1489
+ return {
1490
+ args,
1491
+ env: launchEnv,
1492
+ executablePath
1493
+ };
1494
+ };
1495
+
1328
1496
  const SIGINT = 'SIGINT';
1329
1497
 
1498
+ const devtoolsRegex = /^DevTools listening on (ws:\/\/.*)$/;
1499
+ const waitForDevtoolsEndpoint = async child => {
1500
+ const {
1501
+ stderr
1502
+ } = child;
1503
+ if (!stderr) {
1504
+ throw new Error('Electron stderr is unavailable');
1505
+ }
1506
+ const lines = createInterface({
1507
+ input: stderr
1508
+ });
1509
+ const {
1510
+ promise,
1511
+ reject,
1512
+ resolve
1513
+ } = Promise.withResolvers();
1514
+ const onExit = () => {
1515
+ reject(new Error('Electron exited before DevTools endpoint was available'));
1516
+ };
1517
+ const onLine = line => {
1518
+ const match = line.match(devtoolsRegex);
1519
+ if (match) {
1520
+ resolve(match[1]);
1521
+ }
1522
+ };
1523
+ child.once('exit', onExit);
1524
+ lines.on('line', onLine);
1525
+ try {
1526
+ return await promise;
1527
+ } finally {
1528
+ child.off('exit', onExit);
1529
+ lines.off('line', onLine);
1530
+ lines.close();
1531
+ }
1532
+ };
1533
+ const getFirstPage = async browser => {
1534
+ const context = browser.contexts()[0];
1535
+ const pages = context.pages();
1536
+ if (pages.length > 0) {
1537
+ return pages[0];
1538
+ }
1539
+ return context.waitForEvent('page', {
1540
+ timeout: 15_000
1541
+ });
1542
+ };
1543
+ const closeElectron = async ({
1544
+ browser,
1545
+ child
1546
+ }) => {
1547
+ try {
1548
+ await browser.close();
1549
+ } catch {
1550
+ // ignore close errors during cleanup
1551
+ }
1552
+ if (!child.killed) {
1553
+ child.kill(SIGINT);
1554
+ }
1555
+ };
1556
+ const createElectronLaunch = ({
1557
+ browser,
1558
+ child,
1559
+ page,
1560
+ signal
1561
+ }) => {
1562
+ let disposed = false;
1563
+ const dispose = async () => {
1564
+ if (disposed) {
1565
+ return;
1566
+ }
1567
+ disposed = true;
1568
+ signal.removeEventListener('abort', handleAbort);
1569
+ process.off('SIGINT', handleSigint);
1570
+ process.off('SIGTERM', handleSigterm);
1571
+ await closeElectron({
1572
+ browser,
1573
+ child
1574
+ });
1575
+ };
1576
+ const handleAbort = () => {
1577
+ void dispose();
1578
+ };
1579
+ const handleProcessSignal = processSignal => {
1580
+ void (async () => {
1581
+ try {
1582
+ await dispose();
1583
+ } finally {
1584
+ process.kill(process.pid, processSignal);
1585
+ }
1586
+ })();
1587
+ };
1588
+ const handleSigint = () => {
1589
+ handleProcessSignal('SIGINT');
1590
+ };
1591
+ const handleSigterm = () => {
1592
+ handleProcessSignal('SIGTERM');
1593
+ };
1594
+ signal.addEventListener('abort', handleAbort);
1595
+ process.once('SIGINT', handleSigint);
1596
+ process.once('SIGTERM', handleSigterm);
1597
+ const electronApp = {
1598
+ close: dispose,
1599
+ process: () => {
1600
+ return child;
1601
+ },
1602
+ [Symbol.asyncDispose]: dispose
1603
+ };
1604
+ return {
1605
+ electronApp,
1606
+ page,
1607
+ [Symbol.asyncDispose]: dispose
1608
+ };
1609
+ };
1610
+ const startElectron = async ({
1611
+ runtimeOptions,
1612
+ signal
1613
+ }) => {
1614
+ const launchOptions = getElectronLaunchOptions(runtimeOptions);
1615
+ const child = spawn(launchOptions.executablePath, ['--remote-debugging-port=0', ...launchOptions.args], {
1616
+ env: launchOptions.env,
1617
+ stdio: ['ignore', 'ignore', 'pipe']
1618
+ });
1619
+ let browser;
1620
+ try {
1621
+ const endpoint = await waitForDevtoolsEndpoint(child);
1622
+ browser = await chromium.connectOverCDP(endpoint);
1623
+ const page = await getFirstPage(browser);
1624
+ return createElectronLaunch({
1625
+ browser,
1626
+ child,
1627
+ page,
1628
+ signal
1629
+ });
1630
+ } catch (error) {
1631
+ if (browser) {
1632
+ try {
1633
+ await browser.close();
1634
+ } catch {
1635
+ // ignore close errors during cleanup
1636
+ }
1637
+ }
1638
+ if (!child.killed) {
1639
+ child.kill(SIGINT);
1640
+ }
1641
+ throw error;
1642
+ }
1643
+ };
1644
+
1330
1645
  const tearDownTests = async ({
1331
1646
  child,
1332
- controller
1647
+ controller,
1648
+ kill
1333
1649
  }) => {
1334
1650
  controller.abort();
1335
- child.kill(SIGINT);
1651
+ if (kill) {
1652
+ await kill();
1653
+ return;
1654
+ }
1655
+ if (child) {
1656
+ child.kill(SIGINT);
1657
+ }
1336
1658
  };
1337
1659
 
1338
1660
  /**
@@ -1346,31 +1668,19 @@ const tearDownTests = async ({
1346
1668
  * @param {boolean} traceFocus
1347
1669
  * @param {string} filter
1348
1670
  */
1349
- const runAllTests = async (extensionPath, testPath, cwd, browser, headless, timeout, serverPath, traceFocus, filter) => {
1671
+ const runAllTests = async (extensionPath, testPath, cwd, browser, headless, timeout, runtimeOptions, traceFocus, filter) => {
1350
1672
  string(extensionPath);
1351
1673
  string(testPath);
1352
1674
  string(cwd);
1353
1675
  string(browser);
1354
1676
  boolean(headless);
1355
1677
  number(timeout);
1678
+ object(runtimeOptions);
1356
1679
  const rpc = get(Cli);
1357
1680
  const controller = new AbortController();
1358
1681
  const {
1359
1682
  signal
1360
1683
  } = controller;
1361
- // @ts-ignore
1362
- const {
1363
- child,
1364
- page,
1365
- port
1366
- } = await setupTests({
1367
- browser,
1368
- headless,
1369
- onlyExtension: extensionPath,
1370
- serverPath,
1371
- signal,
1372
- testPath
1373
- });
1374
1684
  const testSrc = join(testPath, 'src');
1375
1685
  const tests = await getTests(testSrc);
1376
1686
  const onResult = async result => {
@@ -1382,6 +1692,37 @@ const runAllTests = async (extensionPath, testPath, cwd, browser, headless, time
1382
1692
  const filterOption = filter === undefined ? undefined : {
1383
1693
  filter
1384
1694
  };
1695
+ if (runtimeOptions.type === 'electron') {
1696
+ await using electron = await startElectron({
1697
+ runtimeOptions,
1698
+ signal
1699
+ });
1700
+ await runElectronTests({
1701
+ ...filterOption,
1702
+ electronApp: electron.electronApp,
1703
+ onFinalResult,
1704
+ onResult,
1705
+ page: electron.page,
1706
+ tests,
1707
+ testSrc,
1708
+ timeout
1709
+ });
1710
+ return;
1711
+ }
1712
+ const {
1713
+ child,
1714
+ page,
1715
+ port
1716
+ } = await setupTests({
1717
+ browser,
1718
+ headless,
1719
+ onlyExtension: extensionPath,
1720
+ ...(runtimeOptions.serverPath && {
1721
+ serverPath: runtimeOptions.serverPath
1722
+ }),
1723
+ signal,
1724
+ testPath
1725
+ });
1385
1726
  await runTests({
1386
1727
  ...filterOption,
1387
1728
  headless,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/test-with-playwright-worker",
3
- "version": "18.1.0",
3
+ "version": "20.0.0",
4
4
  "description": "Worker package for test-with-playwright",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,8 @@
12
12
  "type": "module",
13
13
  "main": "dist/workerMain.js",
14
14
  "dependencies": {
15
- "@playwright/test": "1.61.0"
15
+ "@playwright/test": "1.61.0",
16
+ "get-port": "^7.2.0"
16
17
  },
17
18
  "engines": {
18
19
  "node": ">=24"