pwvm 0.1.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/commands/current.js +42 -0
- package/dist/commands/doctor.js +95 -0
- package/dist/commands/install.js +121 -0
- package/dist/commands/install.test.js +106 -0
- package/dist/commands/list.js +30 -0
- package/dist/commands/listRemote.js +20 -0
- package/dist/commands/prune.js +22 -0
- package/dist/commands/setup.js +38 -0
- package/dist/commands/use.js +31 -0
- package/dist/commands/use.test.js +41 -0
- package/dist/core/browsers.js +28 -0
- package/dist/core/install.js +82 -0
- package/dist/core/install.test.js +97 -0
- package/dist/core/paths.js +37 -0
- package/dist/core/prune.js +71 -0
- package/dist/core/shim.js +32 -0
- package/dist/core/shim.test.js +73 -0
- package/dist/core/shimTemplates.js +6 -0
- package/dist/core/versions.js +72 -0
- package/dist/core/versions.test.js +92 -0
- package/dist/index.js +164 -0
- package/dist/index.test.js +104 -0
- package/dist/test/utils.js +46 -0
- package/dist/test-utils/pwvmTestEnv.js +34 -0
- package/dist/utils/logger.js +64 -0
- package/dist/utils/progress.js +87 -0
- package/dist/utils/registry.js +36 -0
- package/package.json +52 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getVersionsDir } from "./paths.js";
|
|
5
|
+
export const installPlaywrightBrowsers = async (version, options = {}) => {
|
|
6
|
+
const fsImpl = options.fs ?? fs;
|
|
7
|
+
const execaImpl = options.execa ?? execa;
|
|
8
|
+
const resolveVersionsDir = options.getVersionsDir ?? getVersionsDir;
|
|
9
|
+
const versionsDir = options.versionsDir ?? resolveVersionsDir(options.homeDir);
|
|
10
|
+
const installDir = path.join(versionsDir, version);
|
|
11
|
+
const installDirExists = await fsImpl.pathExists(installDir);
|
|
12
|
+
if (!installDirExists) {
|
|
13
|
+
throw new Error(`Playwright ${version} is not installed.`);
|
|
14
|
+
}
|
|
15
|
+
const playwrightDir = path.join(installDir, "node_modules", "playwright");
|
|
16
|
+
const playwrightExists = await fsImpl.pathExists(playwrightDir);
|
|
17
|
+
if (!playwrightExists) {
|
|
18
|
+
throw new Error(`Playwright ${version} is not installed.`);
|
|
19
|
+
}
|
|
20
|
+
const browsersDir = path.join(installDir, "browsers");
|
|
21
|
+
await execaImpl("npx", ["playwright", "install"], {
|
|
22
|
+
cwd: installDir,
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
PLAYWRIGHT_BROWSERS_PATH: browsersDir,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import semver from "semver";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { installPlaywrightBrowsers } from "./browsers.js";
|
|
6
|
+
import { getVersionsDir } from "./paths.js";
|
|
7
|
+
import { getInstalledVersions, } from "./versions.js";
|
|
8
|
+
export const installPlaywrightVersion = async (version, options = {}) => {
|
|
9
|
+
if (!semver.valid(version)) {
|
|
10
|
+
throw new Error(`Invalid Playwright version "${version}".`);
|
|
11
|
+
}
|
|
12
|
+
const fsImpl = options.fs ?? fs;
|
|
13
|
+
const execaImpl = options.execa ?? execa;
|
|
14
|
+
const resolveVersionsDir = options.getVersionsDir ?? getVersionsDir;
|
|
15
|
+
const getInstalled = options.getInstalled ?? getInstalledVersions;
|
|
16
|
+
const installBrowsers = options.installBrowsers ?? installPlaywrightBrowsers;
|
|
17
|
+
const versionsDir = options.versionsDir ?? resolveVersionsDir(options.homeDir);
|
|
18
|
+
const targetDir = path.join(versionsDir, version);
|
|
19
|
+
const packageDir = path.join(targetDir, "node_modules", "playwright");
|
|
20
|
+
const browsersDir = path.join(targetDir, "browsers");
|
|
21
|
+
let packageExists = false;
|
|
22
|
+
let browsersExists = false;
|
|
23
|
+
let cleanupOnFailure = false;
|
|
24
|
+
const result = {
|
|
25
|
+
packageInstalled: false,
|
|
26
|
+
browsersInstalled: false,
|
|
27
|
+
packageAlreadyInstalled: false,
|
|
28
|
+
browsersAlreadyInstalled: false,
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
packageExists = await fsImpl.pathExists(packageDir);
|
|
32
|
+
browsersExists = await fsImpl.pathExists(browsersDir);
|
|
33
|
+
cleanupOnFailure = !packageExists;
|
|
34
|
+
result.packageAlreadyInstalled = packageExists;
|
|
35
|
+
result.browsersAlreadyInstalled = browsersExists;
|
|
36
|
+
let isFirstInstall = false;
|
|
37
|
+
if (!packageExists && options.withBrowsers === undefined) {
|
|
38
|
+
const installedVersions = await getInstalled({
|
|
39
|
+
homeDir: options.homeDir,
|
|
40
|
+
versionsDir: options.versionsDir,
|
|
41
|
+
...options.installedOptions,
|
|
42
|
+
});
|
|
43
|
+
isFirstInstall = installedVersions.length === 0;
|
|
44
|
+
}
|
|
45
|
+
if (!packageExists) {
|
|
46
|
+
await fsImpl.ensureDir(targetDir);
|
|
47
|
+
await execaImpl("npm", ["install", `playwright@${version}`, "--no-save"], {
|
|
48
|
+
cwd: targetDir,
|
|
49
|
+
env: {
|
|
50
|
+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
result.packageInstalled = true;
|
|
54
|
+
result.packageAlreadyInstalled = false;
|
|
55
|
+
}
|
|
56
|
+
const shouldInstallBrowsers = (!browsersExists && options.withBrowsers === true) ||
|
|
57
|
+
(options.withBrowsers === undefined && !packageExists && isFirstInstall);
|
|
58
|
+
if (shouldInstallBrowsers) {
|
|
59
|
+
await installBrowsers(version, {
|
|
60
|
+
homeDir: options.homeDir,
|
|
61
|
+
versionsDir: options.versionsDir,
|
|
62
|
+
fs: options.fs,
|
|
63
|
+
execa: options.execa,
|
|
64
|
+
getVersionsDir: options.getVersionsDir,
|
|
65
|
+
});
|
|
66
|
+
result.browsersInstalled = true;
|
|
67
|
+
result.browsersAlreadyInstalled = false;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (cleanupOnFailure) {
|
|
73
|
+
try {
|
|
74
|
+
await fsImpl.remove(targetDir);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Best-effort cleanup.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { installPlaywrightVersion } from "./install.js";
|
|
5
|
+
import { createFsMock, createTempPwvmHome } from "../test/utils.js";
|
|
6
|
+
const createExecaMock = () => vi.fn((_file, _args, _options) => Promise.resolve({ exitCode: 0 }));
|
|
7
|
+
describe("installPlaywrightVersion", () => {
|
|
8
|
+
it("installs package when version is not installed", async () => {
|
|
9
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
10
|
+
try {
|
|
11
|
+
const version = "1.2.3";
|
|
12
|
+
const versionsDir = path.join(homeDir, ".pwvm", "versions");
|
|
13
|
+
const targetDir = path.join(versionsDir, version);
|
|
14
|
+
const execaMock = createExecaMock();
|
|
15
|
+
const fsMock = createFsMock(homeDir);
|
|
16
|
+
await installPlaywrightVersion(version, {
|
|
17
|
+
fs: fsMock,
|
|
18
|
+
execa: execaMock,
|
|
19
|
+
versionsDir,
|
|
20
|
+
withBrowsers: false,
|
|
21
|
+
});
|
|
22
|
+
expect(execaMock).toHaveBeenCalledWith("npm", ["install", `playwright@${version}`, "--no-save"], expect.objectContaining({ cwd: targetDir }));
|
|
23
|
+
expect(await fs.pathExists(targetDir)).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await cleanup();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it("errors on invalid version", async () => {
|
|
30
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
31
|
+
const execaMock = createExecaMock();
|
|
32
|
+
try {
|
|
33
|
+
await expect(installPlaywrightVersion("not-a-version", {
|
|
34
|
+
homeDir,
|
|
35
|
+
execa: execaMock,
|
|
36
|
+
})).rejects.toThrow('Invalid Playwright version "not-a-version".');
|
|
37
|
+
expect(execaMock).not.toHaveBeenCalled();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await cleanup();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
it("allows browser installation on existing package", async () => {
|
|
44
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
45
|
+
try {
|
|
46
|
+
const version = "2.0.0";
|
|
47
|
+
const versionsDir = path.join(homeDir, ".pwvm", "versions");
|
|
48
|
+
const targetDir = path.join(versionsDir, version);
|
|
49
|
+
const packageDir = path.join(targetDir, "node_modules", "playwright");
|
|
50
|
+
const execaMock = createExecaMock();
|
|
51
|
+
const installBrowsers = vi.fn(async () => { });
|
|
52
|
+
const fsMock = createFsMock(homeDir);
|
|
53
|
+
await fs.ensureDir(packageDir);
|
|
54
|
+
await installPlaywrightVersion(version, {
|
|
55
|
+
fs: fsMock,
|
|
56
|
+
execa: execaMock,
|
|
57
|
+
versionsDir,
|
|
58
|
+
withBrowsers: true,
|
|
59
|
+
installBrowsers,
|
|
60
|
+
});
|
|
61
|
+
expect(installBrowsers).toHaveBeenCalledWith(version, expect.objectContaining({ versionsDir }));
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await cleanup();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
it("does not reinstall package twice", async () => {
|
|
68
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
69
|
+
try {
|
|
70
|
+
const version = "3.1.4";
|
|
71
|
+
const versionsDir = path.join(homeDir, ".pwvm", "versions");
|
|
72
|
+
const targetDir = path.join(versionsDir, version);
|
|
73
|
+
const packageDir = path.join(targetDir, "node_modules", "playwright");
|
|
74
|
+
const browsersDir = path.join(targetDir, "browsers");
|
|
75
|
+
const execaMock = createExecaMock();
|
|
76
|
+
const fsMock = createFsMock(homeDir);
|
|
77
|
+
await installPlaywrightVersion(version, {
|
|
78
|
+
fs: fsMock,
|
|
79
|
+
execa: execaMock,
|
|
80
|
+
versionsDir,
|
|
81
|
+
withBrowsers: false,
|
|
82
|
+
});
|
|
83
|
+
await fs.ensureDir(packageDir);
|
|
84
|
+
await fs.ensureDir(browsersDir);
|
|
85
|
+
await installPlaywrightVersion(version, {
|
|
86
|
+
fs: fsMock,
|
|
87
|
+
execa: execaMock,
|
|
88
|
+
versionsDir,
|
|
89
|
+
withBrowsers: false,
|
|
90
|
+
});
|
|
91
|
+
expect(execaMock).toHaveBeenCalledTimes(1);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await cleanup();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const PWVM_DIR_NAME = ".pwvm";
|
|
5
|
+
export const PWVM_RC_NAME = ".pwvmrc";
|
|
6
|
+
export const getPwvmDir = (homeDir = os.homedir()) => path.join(homeDir, PWVM_DIR_NAME);
|
|
7
|
+
export const getVersionsDir = (homeDir = os.homedir()) => path.join(getPwvmDir(homeDir), "versions");
|
|
8
|
+
export const getShimsDir = (homeDir = os.homedir()) => path.join(getPwvmDir(homeDir), "shims");
|
|
9
|
+
export const getActiveVersionFile = (homeDir = os.homedir()) => path.join(getPwvmDir(homeDir), "version");
|
|
10
|
+
export const createPaths = (homeDir = os.homedir()) => ({
|
|
11
|
+
homeDir,
|
|
12
|
+
pwvmDir: getPwvmDir(homeDir),
|
|
13
|
+
versionsDir: getVersionsDir(homeDir),
|
|
14
|
+
shimsDir: getShimsDir(homeDir),
|
|
15
|
+
activeVersionFile: getActiveVersionFile(homeDir),
|
|
16
|
+
});
|
|
17
|
+
export const resolveRcPath = (options = {}) => {
|
|
18
|
+
const startDir = options.startDir ?? process.cwd();
|
|
19
|
+
const rcFileName = options.rcFileName ?? PWVM_RC_NAME;
|
|
20
|
+
const fsImpl = options.fs ?? fs;
|
|
21
|
+
let currentDir = path.resolve(startDir);
|
|
22
|
+
while (true) {
|
|
23
|
+
const candidate = path.join(currentDir, rcFileName);
|
|
24
|
+
if (fsImpl.existsSync(candidate)) {
|
|
25
|
+
const stat = fsImpl.statSync(candidate);
|
|
26
|
+
if (stat.isFile()) {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const parentDir = path.dirname(currentDir);
|
|
31
|
+
if (parentDir === currentDir) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
currentDir = parentDir;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getVersionsDir, resolveRcPath } from "./paths.js";
|
|
4
|
+
import { getGlobalActiveVersion, getInstalledVersions, } from "./versions.js";
|
|
5
|
+
const readRcVersion = async (options, fsImpl, resolveRc) => {
|
|
6
|
+
try {
|
|
7
|
+
const rcPath = resolveRc({
|
|
8
|
+
startDir: options.rcStartDir,
|
|
9
|
+
rcFileName: options.rcFileName,
|
|
10
|
+
fs: fsImpl,
|
|
11
|
+
});
|
|
12
|
+
if (!rcPath) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const contents = await fsImpl.readFile(rcPath, "utf8");
|
|
16
|
+
const trimmed = contents.trim();
|
|
17
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const pruneVersions = async (options = {}) => {
|
|
24
|
+
const fsImpl = (options.fs ?? fs);
|
|
25
|
+
const getInstalled = options.getInstalled ?? getInstalledVersions;
|
|
26
|
+
const getGlobalActive = options.getGlobalActive ?? getGlobalActiveVersion;
|
|
27
|
+
const resolveRc = options.resolveRc ?? resolveRcPath;
|
|
28
|
+
const resolveVersionsDir = options.getVersionsDir ?? getVersionsDir;
|
|
29
|
+
const [installedVersions, globalVersion, rcVersion] = await Promise.all([
|
|
30
|
+
getInstalled({
|
|
31
|
+
homeDir: options.homeDir,
|
|
32
|
+
versionsDir: options.versionsDir,
|
|
33
|
+
fs: fsImpl,
|
|
34
|
+
...options.installedOptions,
|
|
35
|
+
}),
|
|
36
|
+
getGlobalActive({
|
|
37
|
+
homeDir: options.homeDir,
|
|
38
|
+
activeVersionFile: options.activeVersionFile,
|
|
39
|
+
fs: fsImpl,
|
|
40
|
+
...options.globalOptions,
|
|
41
|
+
}),
|
|
42
|
+
readRcVersion(options, fsImpl, resolveRc),
|
|
43
|
+
]);
|
|
44
|
+
const keep = new Set();
|
|
45
|
+
if (globalVersion) {
|
|
46
|
+
keep.add(globalVersion);
|
|
47
|
+
}
|
|
48
|
+
if (rcVersion) {
|
|
49
|
+
keep.add(rcVersion);
|
|
50
|
+
}
|
|
51
|
+
const versionsDir = options.versionsDir ?? resolveVersionsDir(options.homeDir);
|
|
52
|
+
const removed = [];
|
|
53
|
+
for (const version of installedVersions) {
|
|
54
|
+
if (keep.has(version)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const targetDir = path.join(versionsDir, version);
|
|
58
|
+
try {
|
|
59
|
+
const exists = await fsImpl.pathExists(targetDir);
|
|
60
|
+
if (!exists) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
await fsImpl.remove(targetDir);
|
|
64
|
+
removed.push(version);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return removed;
|
|
71
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getVersionsDir } from "./paths.js";
|
|
5
|
+
import { getResolvedActiveVersion, } from "./versions.js";
|
|
6
|
+
export const runPlaywrightShim = async (args, options = {}) => {
|
|
7
|
+
const resolveVersionsDir = options.getVersionsDir ?? getVersionsDir;
|
|
8
|
+
const execaImpl = options.execa ?? execa;
|
|
9
|
+
const fsImpl = options.fs ?? fs;
|
|
10
|
+
const resolveActive = options.getResolvedActiveVersion ?? getResolvedActiveVersion;
|
|
11
|
+
const activeVersion = await resolveActive(options.activeOptions);
|
|
12
|
+
if (!activeVersion) {
|
|
13
|
+
throw new Error("No active Playwright version. Run `pwvm use <version>`.");
|
|
14
|
+
}
|
|
15
|
+
const versionsDir = options.versionsDir ?? resolveVersionsDir(options.homeDir);
|
|
16
|
+
const versionDir = path.join(versionsDir, activeVersion);
|
|
17
|
+
const binaryPath = path.join(versionDir, "node_modules", ".bin", "playwright");
|
|
18
|
+
const binaryExists = await fsImpl.pathExists(binaryPath);
|
|
19
|
+
if (!binaryExists) {
|
|
20
|
+
throw new Error(`Playwright ${activeVersion} is not installed.`);
|
|
21
|
+
}
|
|
22
|
+
const browsersDir = path.join(versionDir, "browsers");
|
|
23
|
+
const result = await execaImpl(binaryPath, args, {
|
|
24
|
+
stdio: "inherit",
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
PLAYWRIGHT_BROWSERS_PATH: browsersDir,
|
|
28
|
+
},
|
|
29
|
+
reject: false,
|
|
30
|
+
});
|
|
31
|
+
process.exitCode = result.exitCode ?? 1;
|
|
32
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { runPlaywrightShim } from "./shim.js";
|
|
5
|
+
import { createFsMock, createTempPwvmHome } from "../test/utils.js";
|
|
6
|
+
const createExecaMock = () => vi.fn((_file, _args, _options) => Promise.resolve({ exitCode: 0 }));
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
process.exitCode = undefined;
|
|
9
|
+
});
|
|
10
|
+
describe("runPlaywrightShim", () => {
|
|
11
|
+
it("errors when no active version", async () => {
|
|
12
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
13
|
+
const execaMock = createExecaMock();
|
|
14
|
+
try {
|
|
15
|
+
await expect(runPlaywrightShim(["install"], {
|
|
16
|
+
fs: createFsMock(homeDir),
|
|
17
|
+
execa: execaMock,
|
|
18
|
+
getResolvedActiveVersion: async () => null,
|
|
19
|
+
})).rejects.toThrow("No active Playwright version. Run `pwvm use <version>`.");
|
|
20
|
+
expect(execaMock).not.toHaveBeenCalled();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await cleanup();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
it("resolves correct Playwright binary", async () => {
|
|
27
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
28
|
+
try {
|
|
29
|
+
const version = "1.42.0";
|
|
30
|
+
const versionsDir = path.join(homeDir, ".pwvm", "versions");
|
|
31
|
+
const expectedBinary = path.join(versionsDir, version, "node_modules", ".bin", "playwright");
|
|
32
|
+
const execaMock = createExecaMock();
|
|
33
|
+
await fs.ensureDir(path.dirname(expectedBinary));
|
|
34
|
+
await fs.writeFile(expectedBinary, "");
|
|
35
|
+
await runPlaywrightShim(["install"], {
|
|
36
|
+
fs: createFsMock(homeDir),
|
|
37
|
+
execa: execaMock,
|
|
38
|
+
versionsDir,
|
|
39
|
+
getResolvedActiveVersion: async () => version,
|
|
40
|
+
});
|
|
41
|
+
expect(execaMock).toHaveBeenCalledWith(expectedBinary, ["install"], expect.any(Object));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await cleanup();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it("injects PLAYWRIGHT_BROWSERS_PATH", async () => {
|
|
48
|
+
const { homeDir, cleanup } = await createTempPwvmHome();
|
|
49
|
+
try {
|
|
50
|
+
const version = "1.42.0";
|
|
51
|
+
const versionsDir = path.join(homeDir, ".pwvm", "versions");
|
|
52
|
+
const expectedBinary = path.join(versionsDir, version, "node_modules", ".bin", "playwright");
|
|
53
|
+
const expectedBrowsers = path.join(versionsDir, version, "browsers");
|
|
54
|
+
const execaMock = createExecaMock();
|
|
55
|
+
await fs.ensureDir(path.dirname(expectedBinary));
|
|
56
|
+
await fs.writeFile(expectedBinary, "");
|
|
57
|
+
await runPlaywrightShim(["install"], {
|
|
58
|
+
fs: createFsMock(homeDir),
|
|
59
|
+
execa: execaMock,
|
|
60
|
+
versionsDir,
|
|
61
|
+
getResolvedActiveVersion: async () => version,
|
|
62
|
+
});
|
|
63
|
+
expect(execaMock).toHaveBeenCalledWith(expectedBinary, ["install"], expect.objectContaining({
|
|
64
|
+
env: expect.objectContaining({
|
|
65
|
+
PLAYWRIGHT_BROWSERS_PATH: expectedBrowsers,
|
|
66
|
+
}),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await cleanup();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import { getActiveVersionFile, getPwvmDir, getVersionsDir, resolveRcPath, } from "./paths.js";
|
|
4
|
+
const resolvePwvmDir = (options) => options.pwvmDir ?? getPwvmDir(options.homeDir);
|
|
5
|
+
const resolveVersionsDir = (options) => options.versionsDir ?? getVersionsDir(options.homeDir);
|
|
6
|
+
const resolveActiveVersionFile = (options) => options.activeVersionFile ?? getActiveVersionFile(options.homeDir);
|
|
7
|
+
export const getInstalledVersions = async (options = {}) => {
|
|
8
|
+
const fsImpl = options.fs ?? fs;
|
|
9
|
+
const versionsDir = resolveVersionsDir(options);
|
|
10
|
+
try {
|
|
11
|
+
const exists = await fsImpl.pathExists(versionsDir);
|
|
12
|
+
if (!exists) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const entries = await fsImpl.readdir(versionsDir, { withFileTypes: true });
|
|
16
|
+
const versions = entries
|
|
17
|
+
.filter((entry) => entry.isDirectory())
|
|
18
|
+
.map((entry) => entry.name)
|
|
19
|
+
.filter((version) => semver.valid(version) !== null);
|
|
20
|
+
return versions.sort((a, b) => semver.compare(a, b));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const getGlobalActiveVersion = async (options = {}) => {
|
|
27
|
+
const fsImpl = options.fs ?? fs;
|
|
28
|
+
const activeVersionFile = resolveActiveVersionFile(options);
|
|
29
|
+
try {
|
|
30
|
+
const exists = await fsImpl.pathExists(activeVersionFile);
|
|
31
|
+
if (!exists) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const contents = await fsImpl.readFile(activeVersionFile, "utf8");
|
|
35
|
+
const trimmed = contents.trim();
|
|
36
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export const getResolvedActiveVersion = async (options = {}) => {
|
|
43
|
+
const fsImpl = options.fs ?? fs;
|
|
44
|
+
const resolveRc = options.resolveRc ?? resolveRcPath;
|
|
45
|
+
try {
|
|
46
|
+
const rcPath = resolveRc({
|
|
47
|
+
startDir: options.rcStartDir,
|
|
48
|
+
rcFileName: options.rcFileName,
|
|
49
|
+
fs: fsImpl,
|
|
50
|
+
});
|
|
51
|
+
if (rcPath) {
|
|
52
|
+
const contents = await fsImpl.readFile(rcPath, "utf8");
|
|
53
|
+
const trimmed = contents.trim();
|
|
54
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return getGlobalActiveVersion(options);
|
|
61
|
+
};
|
|
62
|
+
export const setGlobalActiveVersion = async (version, options = {}) => {
|
|
63
|
+
const fsImpl = options.fs ?? fs;
|
|
64
|
+
const installedVersions = await getInstalledVersions(options);
|
|
65
|
+
if (!installedVersions.includes(version)) {
|
|
66
|
+
throw new Error(`Playwright version "${version}" is not installed.`);
|
|
67
|
+
}
|
|
68
|
+
const pwvmDir = resolvePwvmDir(options);
|
|
69
|
+
const activeVersionFile = resolveActiveVersionFile(options);
|
|
70
|
+
await fsImpl.ensureDir(pwvmDir);
|
|
71
|
+
await fsImpl.writeFile(activeVersionFile, `${version}\n`, "utf8");
|
|
72
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getInstalledVersions, getResolvedActiveVersion, } from "./versions.js";
|
|
3
|
+
const dirent = (name, isDirectory) => ({
|
|
4
|
+
name,
|
|
5
|
+
isDirectory: () => isDirectory,
|
|
6
|
+
});
|
|
7
|
+
const makeFs = (overrides = {}) => ({
|
|
8
|
+
pathExists: async (_path) => false,
|
|
9
|
+
readdir: async (_path, _options) => [],
|
|
10
|
+
readFile: async (_path) => "",
|
|
11
|
+
writeFile: async () => { },
|
|
12
|
+
ensureDir: async () => { },
|
|
13
|
+
existsSync: () => false,
|
|
14
|
+
statSync: (_path) => ({ isFile: () => false }),
|
|
15
|
+
...overrides,
|
|
16
|
+
});
|
|
17
|
+
describe("getInstalledVersions", () => {
|
|
18
|
+
it("returns empty list when no versions exist", async () => {
|
|
19
|
+
const fs = makeFs({ pathExists: async (_path) => false });
|
|
20
|
+
await expect(getInstalledVersions({ fs, versionsDir: "/versions" })).resolves.toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
it("reads installed versions correctly", async () => {
|
|
23
|
+
const fs = makeFs({
|
|
24
|
+
pathExists: async (_path) => true,
|
|
25
|
+
readdir: async (_path) => [
|
|
26
|
+
dirent("1.0.0", true),
|
|
27
|
+
dirent("2.0.0", false),
|
|
28
|
+
dirent("not-a-version", true),
|
|
29
|
+
dirent("1.2.3", true),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
const versions = await getInstalledVersions({
|
|
33
|
+
fs,
|
|
34
|
+
versionsDir: "/versions",
|
|
35
|
+
});
|
|
36
|
+
expect(versions).toEqual(["1.0.0", "1.2.3"]);
|
|
37
|
+
});
|
|
38
|
+
it("sorts versions with semver", async () => {
|
|
39
|
+
const fs = makeFs({
|
|
40
|
+
pathExists: async (_path) => true,
|
|
41
|
+
readdir: async (_path) => [
|
|
42
|
+
dirent("1.10.0", true),
|
|
43
|
+
dirent("0.9.0", true),
|
|
44
|
+
dirent("1.2.0", true),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
const versions = await getInstalledVersions({
|
|
48
|
+
fs,
|
|
49
|
+
versionsDir: "/versions",
|
|
50
|
+
});
|
|
51
|
+
expect(versions).toEqual(["0.9.0", "1.2.0", "1.10.0"]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("getResolvedActiveVersion", () => {
|
|
55
|
+
it("respects .pwvmrc override", async () => {
|
|
56
|
+
const rcPath = "/repo/.pwvmrc";
|
|
57
|
+
const globalPath = "/home/.pwvm/version";
|
|
58
|
+
const fs = makeFs({
|
|
59
|
+
pathExists: async (path) => String(path) === globalPath,
|
|
60
|
+
readFile: async (path) => {
|
|
61
|
+
if (String(path) === rcPath) {
|
|
62
|
+
return "1.9.0\n";
|
|
63
|
+
}
|
|
64
|
+
if (String(path) === globalPath) {
|
|
65
|
+
return "2.0.0\n";
|
|
66
|
+
}
|
|
67
|
+
return "";
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const resolveRc = () => rcPath;
|
|
71
|
+
const version = await getResolvedActiveVersion({
|
|
72
|
+
fs,
|
|
73
|
+
resolveRc,
|
|
74
|
+
activeVersionFile: globalPath,
|
|
75
|
+
});
|
|
76
|
+
expect(version).toBe("1.9.0");
|
|
77
|
+
});
|
|
78
|
+
it("falls back to global version", async () => {
|
|
79
|
+
const globalPath = "/home/.pwvm/version";
|
|
80
|
+
const fs = makeFs({
|
|
81
|
+
pathExists: async (path) => String(path) === globalPath,
|
|
82
|
+
readFile: async (path) => String(path) === globalPath ? "2.4.0\n" : "",
|
|
83
|
+
});
|
|
84
|
+
const resolveRc = () => null;
|
|
85
|
+
const version = await getResolvedActiveVersion({
|
|
86
|
+
fs,
|
|
87
|
+
resolveRc,
|
|
88
|
+
activeVersionFile: globalPath,
|
|
89
|
+
});
|
|
90
|
+
expect(version).toBe("2.4.0");
|
|
91
|
+
});
|
|
92
|
+
});
|