@lvce-editor/test-with-playwright 18.1.0 → 19.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 (3) hide show
  1. package/README.md +21 -1
  2. package/dist/main.js +469 -13
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -5,7 +5,15 @@
5
5
  ```json
6
6
  {
7
7
  "scripts": {
8
- "e2e": "node ./node_modules/@lvce-editor/test-with-playwright/bin/test-with-playwright.js --only-extension=. --test-path=./e2e"
8
+ "e2e": "node ./node_modules/@lvce-editor/test-with-playwright/bin/test-with-playwright.js --runtime=browser --only-extension=. --test-path=./e2e"
9
+ }
10
+ }
11
+ ```
12
+
13
+ ```json
14
+ {
15
+ "scripts": {
16
+ "e2e:electron": "node ./node_modules/@lvce-editor/test-with-playwright/bin/test-with-playwright.js --runtime=electron --electron-version=v0.84.0 --test-path=./e2e"
9
17
  }
10
18
  }
11
19
  ```
@@ -19,6 +27,7 @@ test('sample.hello-world', async () => {
19
27
 
20
28
  ## CLI Flags
21
29
 
30
+ - `--runtime`: `browser` (default) or `electron`
22
31
  - `--only-extension`: path to the extension under test
23
32
  - `--test-path`: path to the test root
24
33
  - `--server-path`: explicit server entry point
@@ -26,6 +35,17 @@ test('sample.hello-world', async () => {
26
35
  - `--headless`: run Playwright in headless mode
27
36
  - `--browser`: browser engine to launch, either `chromium` or `firefox`
28
37
  - `--trace-focus`: add `traceFocus=true` to test URLs
38
+ - `--electron-path`: path to an existing Electron app executable
39
+ - `--electron-version`: Lvce release version to download, required for `--runtime=electron` when `--electron-path` is not provided
40
+ - `--electron-cache-dir`: directory for downloaded Electron apps, defaults to `.test-with-playwright/electron`
41
+ - `--electron-arg`: extra Electron app argument, can be provided multiple times
42
+ - `--electron-env`: Electron environment variable in `NAME=value` form, can be provided multiple times
43
+
44
+ ## Runtime Notes
45
+
46
+ - `browser` keeps the server-backed HTML test execution flow.
47
+ - `electron` downloads or reuses a Lvce Electron app, launches it with Playwright, and runs each test module against the first window.
48
+ - `--electron-path` skips downloading and is useful for custom local builds.
29
49
 
30
50
  ## Environment Variables
31
51
 
package/dist/main.js CHANGED
@@ -1,4 +1,10 @@
1
- import { join } from 'node:path';
1
+ import { dirname, join, resolve as resolve$1, basename } from 'node:path';
2
+ import { createWriteStream, existsSync } from 'node:fs';
3
+ import { mkdir, readdir, stat, readFile, chmod, writeFile, rm, mkdtemp } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { execFile as execFile$1 } from 'node:child_process';
7
+ import { promisify } from 'node:util';
2
8
  import { pathToFileURL, fileURLToPath } from 'node:url';
3
9
 
4
10
  const HandleResult = 'HandleResult';
@@ -115,12 +121,18 @@ const getHelpMessage = () => {
115
121
 
116
122
  Options:
117
123
  --browser=<browser> Browser to run tests in: chromium or firefox
124
+ --runtime=<runtime> Runtime to run tests in: browser or electron
118
125
  --filter=<pattern> Only run tests matching the filter
119
126
  --headless Run the browser in headless mode
120
127
  --only-extension=<path> Path to the extension under test
121
128
  --server-path=<path> Path to the editor server entrypoint
122
129
  --test-path=<path> Path to the test files
123
130
  --trace-focus Log focus changes while tests run
131
+ --electron-path=<path> Path to an existing Electron app executable
132
+ --electron-version=<ver> Lvce release version to download, for example v0.84.0
133
+ --electron-cache-dir=<p> Directory for downloaded Electron apps
134
+ --electron-arg=<arg> Extra Electron app argument, repeatable
135
+ --electron-env=<env> Electron environment variable as NAME=value, repeatable
124
136
  -h, --help Show this help message
125
137
 
126
138
  Environment:
@@ -365,14 +377,35 @@ function requireMinimist () {
365
377
  var minimistExports = requireMinimist();
366
378
  const parseArgv = /*@__PURE__*/getDefaultExportFromCjs(minimistExports);
367
379
 
380
+ const toCliString = value => {
381
+ if (typeof value === 'string') {
382
+ return value;
383
+ }
384
+ if (typeof value === 'number' || typeof value === 'boolean') {
385
+ return String(value);
386
+ }
387
+ throw new TypeError('expected cli argument value to be a string, number, or boolean');
388
+ };
389
+ const toStringArray = value => {
390
+ if (value === undefined) {
391
+ return undefined;
392
+ }
393
+ if (Array.isArray(value)) {
394
+ return value.map(toCliString);
395
+ }
396
+ return [toCliString(value)];
397
+ };
368
398
  const parseCliArgs = argv => {
369
399
  const parsed = parseArgv(argv);
370
400
  const result = Object.create(null);
371
401
  if (parsed.browser) {
372
- result.browser = parsed.browser;
402
+ result.browser = String(parsed.browser);
403
+ }
404
+ if (parsed.runtime) {
405
+ result.runtime = String(parsed.runtime);
373
406
  }
374
407
  if (parsed.filter) {
375
- result.filter = parsed.filter;
408
+ result.filter = String(parsed.filter);
376
409
  }
377
410
  if (parsed.help || parsed.h) {
378
411
  result.help = true;
@@ -381,13 +414,30 @@ const parseCliArgs = argv => {
381
414
  result.headless = true;
382
415
  }
383
416
  if (parsed['only-extension']) {
384
- result.onlyExtension = parsed['only-extension'];
417
+ result.onlyExtension = String(parsed['only-extension']);
385
418
  }
386
419
  if (parsed['test-path']) {
387
- result.testPath = parsed['test-path'];
420
+ result.testPath = String(parsed['test-path']);
388
421
  }
389
422
  if (parsed['server-path']) {
390
- result.serverPath = parsed['server-path'];
423
+ result.serverPath = String(parsed['server-path']);
424
+ }
425
+ if (parsed['electron-path']) {
426
+ result.electronPath = String(parsed['electron-path']);
427
+ }
428
+ if (parsed['electron-version']) {
429
+ result.electronVersion = String(parsed['electron-version']);
430
+ }
431
+ if (parsed['electron-cache-dir']) {
432
+ result.electronCacheDir = String(parsed['electron-cache-dir']);
433
+ }
434
+ const electronArgs = toStringArray(parsed['electron-arg']);
435
+ if (electronArgs) {
436
+ result.electronArgs = electronArgs;
437
+ }
438
+ const electronEnv = toStringArray(parsed['electron-env']);
439
+ if (electronEnv) {
440
+ result.electronEnv = electronEnv;
391
441
  }
392
442
  if (parsed['trace-focus']) {
393
443
  result.traceFocus = true;
@@ -411,26 +461,404 @@ const defaultOptions = {
411
461
  extensionPath: '',
412
462
  headless: false,
413
463
  help: false,
464
+ runtime: 'browser',
414
465
  testPath: ''
415
466
  };
416
467
  const isBrowser = value => {
417
468
  return value === 'chromium' || value === 'firefox';
418
469
  };
470
+ const isRuntime = value => {
471
+ return value === 'browser' || value === 'electron';
472
+ };
419
473
  const getOptions = ({
420
474
  argv,
421
475
  env
422
476
  }) => {
423
477
  const parsedEnv = parseEnv(env);
424
478
  const parsedArgs = parseCliArgs(argv);
425
- const browser = parsedArgs.browser ?? defaultOptions.browser;
426
- if (!isBrowser(browser)) {
427
- throw new Error(`[test-with-playwright] unsupported browser: ${browser}`);
479
+ if (parsedArgs.browser !== undefined && !isBrowser(parsedArgs.browser)) {
480
+ throw new Error(`[test-with-playwright] unsupported browser: ${parsedArgs.browser}`);
481
+ }
482
+ if (parsedArgs.runtime !== undefined && !isRuntime(parsedArgs.runtime)) {
483
+ throw new Error(`[test-with-playwright] unsupported runtime: ${parsedArgs.runtime}`);
428
484
  }
485
+ const browser = parsedArgs.browser ?? defaultOptions.browser;
486
+ const runtime = parsedArgs.runtime ?? defaultOptions.runtime;
429
487
  return {
430
488
  ...defaultOptions,
431
489
  ...parsedEnv,
432
490
  ...parsedArgs,
433
- browser
491
+ browser,
492
+ runtime
493
+ };
494
+ };
495
+
496
+ const downloadFile = async ({
497
+ to,
498
+ url
499
+ }) => {
500
+ await mkdir(dirname(to), {
501
+ recursive: true
502
+ });
503
+ const response = await fetch(url);
504
+ if (!response.ok || !response.body) {
505
+ throw new Error(`[test-with-playwright] failed to download ${url}: ${response.status} ${response.statusText}`);
506
+ }
507
+ await pipeline(response.body, createWriteStream(to));
508
+ };
509
+
510
+ const execFileAsync = promisify(execFile$1);
511
+ const execFile = async ({
512
+ args,
513
+ command
514
+ }) => {
515
+ await execFileAsync(command, [...args], {
516
+ windowsHide: true
517
+ });
518
+ };
519
+
520
+ const isWindowsExecutable = entry => {
521
+ return entry.toLowerCase().endsWith('.exe') && entry.toLowerCase().includes('lvce');
522
+ };
523
+ const isMacosExecutable = entry => {
524
+ return entry.endsWith('.app');
525
+ };
526
+ const findFile = async (directory, predicate) => {
527
+ const entries = await readdir(directory, {
528
+ withFileTypes: true
529
+ });
530
+ for (const entry of entries) {
531
+ const absolutePath = join(directory, entry.name);
532
+ if (entry.isDirectory()) {
533
+ const nestedResult = await findFile(absolutePath, predicate);
534
+ if (nestedResult) {
535
+ return nestedResult;
536
+ }
537
+ continue;
538
+ }
539
+ if (entry.isFile() && predicate(entry.name)) {
540
+ return absolutePath;
541
+ }
542
+ }
543
+ return undefined;
544
+ };
545
+ const findApp = async directory => {
546
+ const entries = await readdir(directory, {
547
+ withFileTypes: true
548
+ });
549
+ for (const entry of entries) {
550
+ const absolutePath = join(directory, entry.name);
551
+ if (entry.isDirectory() && isMacosExecutable(entry.name)) {
552
+ return absolutePath;
553
+ }
554
+ if (entry.isDirectory()) {
555
+ const nestedResult = await findApp(absolutePath);
556
+ if (nestedResult) {
557
+ return nestedResult;
558
+ }
559
+ }
560
+ }
561
+ return undefined;
562
+ };
563
+ const findMacosExecutable = async directory => {
564
+ const app = await findApp(directory);
565
+ if (!app) {
566
+ return undefined;
567
+ }
568
+ const macosDirectory = join(app, 'Contents', 'MacOS');
569
+ const entries = await readdir(macosDirectory);
570
+ for (const entry of entries) {
571
+ const absolutePath = join(macosDirectory, entry);
572
+ const fileStat = await stat(absolutePath);
573
+ if (fileStat.isFile()) {
574
+ return absolutePath;
575
+ }
576
+ }
577
+ return undefined;
578
+ };
579
+ const findElectronExecutable = async ({
580
+ directory,
581
+ platform
582
+ }) => {
583
+ if (platform === 'darwin') {
584
+ const executable = await findMacosExecutable(directory);
585
+ if (executable) {
586
+ return executable;
587
+ }
588
+ } else if (platform === 'win32') {
589
+ const executable = await findFile(directory, isWindowsExecutable);
590
+ if (executable) {
591
+ return executable;
592
+ }
593
+ } else {
594
+ const executable = await findFile(directory, entry => entry === 'lvce');
595
+ if (executable) {
596
+ return executable;
597
+ }
598
+ }
599
+ throw new Error(`[test-with-playwright] failed to locate Electron executable in ${directory}`);
600
+ };
601
+
602
+ const getLinuxArch = arch => {
603
+ switch (arch) {
604
+ case 'arm':
605
+ return 'armhf';
606
+ case 'arm64':
607
+ return 'arm64';
608
+ case 'x64':
609
+ return 'amd64';
610
+ default:
611
+ throw new Error(`[test-with-playwright] unsupported electron platform: linux/${arch}`);
612
+ }
613
+ };
614
+ const getWindowsArch = arch => {
615
+ switch (arch) {
616
+ case 'arm64':
617
+ return 'arm64';
618
+ case 'x64':
619
+ return 'x64';
620
+ default:
621
+ throw new Error(`[test-with-playwright] unsupported electron platform: win32/${arch}`);
622
+ }
623
+ };
624
+ const resolveElectronAsset = ({
625
+ arch = process.arch,
626
+ platform = process.platform,
627
+ version
628
+ }) => {
629
+ switch (platform) {
630
+ case 'darwin':
631
+ if (arch !== 'arm64') {
632
+ throw new Error(`[test-with-playwright] unsupported electron platform: darwin/${arch}`);
633
+ }
634
+ return {
635
+ archiveType: 'dmg',
636
+ assetName: `lvce-${version}_arm64.dmg`
637
+ };
638
+ case 'linux':
639
+ return {
640
+ archiveType: 'deb',
641
+ assetName: `lvce-${version}_${getLinuxArch(arch)}.deb`
642
+ };
643
+ case 'win32':
644
+ return {
645
+ archiveType: 'exe',
646
+ assetName: `Lvce-Setup-${version}-${getWindowsArch(arch)}.exe`
647
+ };
648
+ default:
649
+ throw new Error(`[test-with-playwright] unsupported electron platform: ${platform}/${arch}`);
650
+ }
651
+ };
652
+
653
+ const getDownloadUrl = ({
654
+ assetName,
655
+ version
656
+ }) => {
657
+ return `https://github.com/lvce-editor/lvce-editor/releases/download/${version}/${assetName}`;
658
+ };
659
+ const prepareLinux = async ({
660
+ assetPath,
661
+ cachePath
662
+ }) => {
663
+ await execFile({
664
+ args: ['-x', assetPath, cachePath],
665
+ command: 'dpkg-deb'
666
+ });
667
+ };
668
+ const prepareMacos = async ({
669
+ assetPath,
670
+ cachePath
671
+ }) => {
672
+ const mountRoot = await mkdtemp(join(tmpdir(), 'test-with-playwright-dmg-'));
673
+ try {
674
+ await execFile({
675
+ args: ['attach', assetPath, '-mountpoint', mountRoot, '-nobrowse', '-quiet'],
676
+ command: 'hdiutil'
677
+ });
678
+ const entries = await readdir(mountRoot);
679
+ const appName = entries.find(entry => entry.endsWith('.app'));
680
+ if (!appName) {
681
+ throw new Error(`[test-with-playwright] failed to locate .app in ${assetPath}`);
682
+ }
683
+ await execFile({
684
+ args: ['-R', join(mountRoot, appName), join(cachePath, appName)],
685
+ command: 'cp'
686
+ });
687
+ } finally {
688
+ try {
689
+ await execFile({
690
+ args: ['detach', mountRoot, '-quiet'],
691
+ command: 'hdiutil'
692
+ });
693
+ } catch {
694
+ // ignore detach errors during cleanup
695
+ }
696
+ await rm(mountRoot, {
697
+ force: true,
698
+ recursive: true
699
+ });
700
+ }
701
+ };
702
+ const prepareWindows = async ({
703
+ assetPath,
704
+ cachePath
705
+ }) => {
706
+ await execFile({
707
+ args: ['/S', `/D=${cachePath}`],
708
+ command: assetPath
709
+ });
710
+ };
711
+ const prepareArchive = async ({
712
+ archiveType,
713
+ assetPath,
714
+ cachePath
715
+ }) => {
716
+ await rm(cachePath, {
717
+ force: true,
718
+ recursive: true
719
+ });
720
+ await mkdir(cachePath, {
721
+ recursive: true
722
+ });
723
+ switch (archiveType) {
724
+ case 'deb':
725
+ await prepareLinux({
726
+ assetPath,
727
+ cachePath
728
+ });
729
+ break;
730
+ case 'dmg':
731
+ await prepareMacos({
732
+ assetPath,
733
+ cachePath
734
+ });
735
+ break;
736
+ case 'exe':
737
+ await prepareWindows({
738
+ assetPath,
739
+ cachePath
740
+ });
741
+ break;
742
+ default:
743
+ throw new Error(`[test-with-playwright] unsupported Electron archive type: ${archiveType}`);
744
+ }
745
+ };
746
+ const prepareElectronApp = async ({
747
+ cacheDir,
748
+ electronPath,
749
+ version
750
+ }) => {
751
+ if (electronPath) {
752
+ return resolve$1(electronPath);
753
+ }
754
+ if (!version) {
755
+ throw new Error('[test-with-playwright] --electron-version is required when --runtime=electron and --electron-path is not provided');
756
+ }
757
+ const {
758
+ archiveType,
759
+ assetName
760
+ } = resolveElectronAsset({
761
+ version
762
+ });
763
+ const cachePath = resolve$1(cacheDir, version, process.platform, process.arch);
764
+ const downloadsPath = join(cachePath, 'downloads');
765
+ const appPath = join(cachePath, 'app');
766
+ const executableMarkerPath = join(cachePath, 'executable-path.txt');
767
+ if (existsSync(executableMarkerPath)) {
768
+ const cachedExecutablePathContent = await readFile(executableMarkerPath, 'utf8');
769
+ const cachedExecutablePath = cachedExecutablePathContent.trim();
770
+ if (existsSync(cachedExecutablePath)) {
771
+ return cachedExecutablePath;
772
+ }
773
+ }
774
+ const assetPath = join(downloadsPath, basename(assetName));
775
+ await mkdir(downloadsPath, {
776
+ recursive: true
777
+ });
778
+ if (!existsSync(assetPath)) {
779
+ await downloadFile({
780
+ to: assetPath,
781
+ url: getDownloadUrl({
782
+ assetName,
783
+ version
784
+ })
785
+ });
786
+ }
787
+ await prepareArchive({
788
+ archiveType,
789
+ assetPath,
790
+ cachePath: appPath
791
+ });
792
+ const executablePath = await findElectronExecutable({
793
+ directory: appPath,
794
+ platform: process.platform
795
+ });
796
+ if (process.platform !== 'win32') {
797
+ await chmod(executablePath, 0o755);
798
+ }
799
+ await writeFile(executableMarkerPath, executablePath);
800
+ return executablePath;
801
+ };
802
+
803
+ const parseElectronEnv = electronEnv => {
804
+ const env = Object.create(null);
805
+ if (!electronEnv) {
806
+ return env;
807
+ }
808
+ for (const item of electronEnv) {
809
+ const index = item.indexOf('=');
810
+ if (index === -1) {
811
+ env[item] = '';
812
+ continue;
813
+ }
814
+ const key = item.slice(0, index);
815
+ const value = item.slice(index + 1);
816
+ env[key] = value;
817
+ }
818
+ return env;
819
+ };
820
+ const getBrowserRuntimeOptions = serverPath => {
821
+ if (serverPath) {
822
+ return {
823
+ serverPath,
824
+ type: 'browser'
825
+ };
826
+ }
827
+ return {
828
+ type: 'browser'
829
+ };
830
+ };
831
+ const getRuntimeOptions = async ({
832
+ cwd,
833
+ electronArgs,
834
+ electronCacheDir,
835
+ electronEnv,
836
+ electronPath,
837
+ electronVersion,
838
+ runtime,
839
+ serverPath
840
+ }) => {
841
+ if (runtime !== undefined && runtime !== 'browser' && runtime !== 'electron') {
842
+ throw new Error(`[test-with-playwright] unsupported runtime: ${runtime}`);
843
+ }
844
+ if (runtime !== 'electron') {
845
+ return getBrowserRuntimeOptions(serverPath);
846
+ }
847
+ const cacheDir = resolve$1(cwd, electronCacheDir || '.test-with-playwright/electron');
848
+ const executablePath = await prepareElectronApp({
849
+ cacheDir,
850
+ ...(electronPath && {
851
+ electronPath: resolve$1(cwd, electronPath)
852
+ }),
853
+ ...(electronVersion && {
854
+ version: electronVersion
855
+ })
856
+ });
857
+ return {
858
+ args: electronArgs || [],
859
+ env: parseElectronEnv(electronEnv),
860
+ executablePath,
861
+ type: 'electron'
434
862
  };
435
863
  };
436
864
 
@@ -1365,7 +1793,7 @@ const runAllTests = async ({
1365
1793
  filter,
1366
1794
  headless,
1367
1795
  onlyExtension,
1368
- serverPath,
1796
+ runtimeOptions,
1369
1797
  testPath,
1370
1798
  testWorkerUri,
1371
1799
  timeout,
@@ -1379,7 +1807,7 @@ const runAllTests = async ({
1379
1807
  path,
1380
1808
  stdio: 'inherit'
1381
1809
  });
1382
- await rpc.invoke(RunAllTests, onlyExtension, testPath, cwd, browser, headless, timeout, serverPath, traceFocus, filter);
1810
+ await rpc.invoke(RunAllTests, onlyExtension, testPath, cwd, browser, headless, timeout, runtimeOptions, traceFocus, filter);
1383
1811
  await rpc.dispose();
1384
1812
  };
1385
1813
 
@@ -1395,10 +1823,16 @@ const handleCliArgs = async ({
1395
1823
  });
1396
1824
  const {
1397
1825
  browser,
1826
+ electronArgs,
1827
+ electronCacheDir,
1828
+ electronEnv,
1829
+ electronPath,
1830
+ electronVersion,
1398
1831
  filter,
1399
1832
  headless,
1400
1833
  help,
1401
1834
  onlyExtension,
1835
+ runtime,
1402
1836
  serverPath,
1403
1837
  testPath,
1404
1838
  traceFocus
@@ -1409,6 +1843,28 @@ const handleCliArgs = async ({
1409
1843
  }
1410
1844
  const timeout = 30_000;
1411
1845
  const testWorkerUri = getTestWorkerUrl();
1846
+ const runtimeOptions = await getRuntimeOptions({
1847
+ cwd,
1848
+ ...(electronArgs && {
1849
+ electronArgs
1850
+ }),
1851
+ ...(electronCacheDir && {
1852
+ electronCacheDir
1853
+ }),
1854
+ ...(electronEnv && {
1855
+ electronEnv
1856
+ }),
1857
+ ...(electronPath && {
1858
+ electronPath
1859
+ }),
1860
+ ...(electronVersion && {
1861
+ electronVersion
1862
+ }),
1863
+ runtime,
1864
+ ...(serverPath && {
1865
+ serverPath
1866
+ })
1867
+ });
1412
1868
  await runAllTests({
1413
1869
  browser,
1414
1870
  commandMap,
@@ -1417,7 +1873,7 @@ const handleCliArgs = async ({
1417
1873
  headless,
1418
1874
  // @ts-ignore
1419
1875
  onlyExtension,
1420
- serverPath,
1876
+ runtimeOptions,
1421
1877
  testPath,
1422
1878
  testWorkerUri,
1423
1879
  timeout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/test-with-playwright",
3
- "version": "18.1.0",
3
+ "version": "19.0.0",
4
4
  "description": "CLI tool for running Playwright tests",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@
12
12
  "main": "dist/main.js",
13
13
  "bin": "bin/test-with-playwright.js",
14
14
  "dependencies": {
15
- "@lvce-editor/test-with-playwright-worker": "18.1.0",
15
+ "@lvce-editor/test-with-playwright-worker": "19.0.0",
16
16
  "@lvce-editor/test-worker": "^16.2.0"
17
17
  },
18
18
  "engines": {