Path: blob/main/tests/unit/tools/chrome-headless-shell.test.ts
12921 views
/*1* chrome-headless-shell.test.ts2*3* Copyright (C) 2026 Posit Software, PBC4*/56import { unitTest } from "../../test.ts";7import { assert, assertEquals } from "testing/asserts";8import { join } from "../../../src/deno_ral/path.ts";9import { existsSync, safeRemoveSync } from "../../../src/deno_ral/fs.ts";10import { isWindows } from "../../../src/deno_ral/platform.ts";11import { runningInCI } from "../../../src/core/ci-info.ts";12import { InstallContext } from "../../../src/tools/types.ts";13import { detectCftPlatform, findCftExecutable } from "../../../src/tools/impl/chrome-for-testing.ts";14import { installableTool, installableTools } from "../../../src/tools/tools.ts";15import {16chromeHeadlessShellInstallable,17chromeHeadlessShellInstallDir,18chromeHeadlessShellExecutablePath,19isInstalled,20noteInstalledVersion,21readInstalledVersion,22} from "../../../src/tools/impl/chrome-headless-shell.ts";2324// -- Step 1: Install directory + executable path --2526unitTest("chromeHeadlessShellInstallDir - path ends with chrome-headless-shell", async () => {27const dir = chromeHeadlessShellInstallDir();28assert(29dir.replace(/\\/g, "/").endsWith("chrome-headless-shell"),30`Expected path ending with chrome-headless-shell, got: ${dir}`,31);32});3334unitTest("chromeHeadlessShellExecutablePath - returns undefined when not installed", async () => {35// If chrome-headless-shell happens to be installed, this test is still valid:36// it should return either a valid path or undefined, never throw.37const result = chromeHeadlessShellExecutablePath();38if (result !== undefined) {39assert(40result.includes("chrome-headless-shell"),41`Expected path containing chrome-headless-shell, got: ${result}`,42);43}44// No assertion failure means the function works correctly either way45});4647// -- Step 2: Version helpers --4849unitTest("version - round-trip write and read", async () => {50const tempDir = Deno.makeTempDirSync();51try {52noteInstalledVersion(tempDir, "145.0.7632.46");53const read = readInstalledVersion(tempDir);54assertEquals(read, "145.0.7632.46");55} finally {56safeRemoveSync(tempDir, { recursive: true });57}58});5960unitTest("version - returns undefined for empty dir", async () => {61const tempDir = Deno.makeTempDirSync();62try {63assertEquals(readInstalledVersion(tempDir), undefined);64} finally {65safeRemoveSync(tempDir, { recursive: true });66}67});6869// -- Step 3: isInstalled() --7071unitTest("isInstalled - returns false when directory is empty", async () => {72const tempDir = Deno.makeTempDirSync();73try {74assertEquals(isInstalled(tempDir), false);75} finally {76safeRemoveSync(tempDir, { recursive: true });77}78});7980unitTest("isInstalled - returns false when only version file exists", async () => {81const tempDir = Deno.makeTempDirSync();82try {83noteInstalledVersion(tempDir, "145.0.0.0");84assertEquals(isInstalled(tempDir), false);85} finally {86safeRemoveSync(tempDir, { recursive: true });87}88});8990unitTest("isInstalled - returns false when only binary exists (no version file)", async () => {91const tempDir = Deno.makeTempDirSync();92try {93const { platform } = detectCftPlatform();94const subdir = join(tempDir, `chrome-headless-shell-${platform}`);95Deno.mkdirSync(subdir);96const binaryName = isWindows ? "chrome-headless-shell.exe" : "chrome-headless-shell";97Deno.writeTextFileSync(join(subdir, binaryName), "fake");98assertEquals(isInstalled(tempDir), false);99} finally {100safeRemoveSync(tempDir, { recursive: true });101}102});103104unitTest("isInstalled - returns true when version file and binary exist", async () => {105const tempDir = Deno.makeTempDirSync();106try {107noteInstalledVersion(tempDir, "145.0.0.0");108const { platform } = detectCftPlatform();109const subdir = join(tempDir, `chrome-headless-shell-${platform}`);110Deno.mkdirSync(subdir);111const binaryName = isWindows ? "chrome-headless-shell.exe" : "chrome-headless-shell";112Deno.writeTextFileSync(join(subdir, binaryName), "fake");113114assertEquals(isInstalled(tempDir), true);115} finally {116safeRemoveSync(tempDir, { recursive: true });117}118});119120// -- Step 4: latestRelease() (external HTTP call, skip on CI) --121122unitTest("latestRelease - returns valid RemotePackageInfo", async () => {123const release = await chromeHeadlessShellInstallable.latestRelease();124assert(release.version, "version should be non-empty");125assert(126/^\d+\.\d+\.\d+\.\d+$/.test(release.version),127`version format wrong: ${release.version}`,128);129assert(release.url.startsWith("https://"), `URL should be https: ${release.url}`);130assert(release.url.includes(release.version), "URL should contain version");131assert(release.assets.length > 0, "should have at least one asset");132assertEquals(release.assets[0].name, "chrome-headless-shell");133}, { ignore: runningInCI() });134135// -- Step 5: preparePackage() (downloads ~50MB, skip on CI) --136137function createMockContext(workingDir: string): InstallContext {138return {139workingDir,140info: (_msg: string) => {},141withSpinner: async (_options, op) => {142await op();143},144error: (_msg: string) => {},145confirm: async (_msg: string) => true,146download: async (_name: string, url: string, target: string) => {147const resp = await fetch(url);148if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);149const data = new Uint8Array(await resp.arrayBuffer());150Deno.writeFileSync(target, data);151},152props: {},153flags: {},154};155}156157unitTest("preparePackage - downloads and extracts chrome-headless-shell", async () => {158const tempDir = Deno.makeTempDirSync();159const ctx = createMockContext(tempDir);160const pkg = await chromeHeadlessShellInstallable.preparePackage(ctx);161try {162assert(pkg.version, "version should be non-empty");163assert(pkg.filePath, "filePath should be non-empty");164const binary = findCftExecutable(pkg.filePath, "chrome-headless-shell");165assert(binary !== undefined, "binary should exist in extracted dir");166} finally {167safeRemoveSync(pkg.filePath, { recursive: true });168safeRemoveSync(tempDir, { recursive: true });169}170}, { ignore: runningInCI() });171172// -- Step 6: afterInstall --173174unitTest("afterInstall - returns false", async () => {175const tempDir = Deno.makeTempDirSync();176const ctx = createMockContext(tempDir);177try {178const result = await chromeHeadlessShellInstallable.afterInstall(ctx);179assertEquals(result, false);180} finally {181safeRemoveSync(tempDir, { recursive: true });182}183});184185// -- Step 7: chromeHeadlessShellInstallable export --186187unitTest("chromeHeadlessShellInstallable - has correct name and methods", async () => {188assertEquals(chromeHeadlessShellInstallable.name, "Chrome Headless Shell");189assertEquals(chromeHeadlessShellInstallable.prereqs.length, 0);190assert(typeof chromeHeadlessShellInstallable.installed === "function");191assert(typeof chromeHeadlessShellInstallable.installDir === "function");192assert(typeof chromeHeadlessShellInstallable.installedVersion === "function");193assert(typeof chromeHeadlessShellInstallable.latestRelease === "function");194assert(typeof chromeHeadlessShellInstallable.preparePackage === "function");195assert(typeof chromeHeadlessShellInstallable.install === "function");196assert(typeof chromeHeadlessShellInstallable.afterInstall === "function");197assert(typeof chromeHeadlessShellInstallable.uninstall === "function");198});199200// -- Integration: full install/uninstall lifecycle --201202unitTest("install lifecycle - prepare, install, verify, uninstall", async () => {203const tool = chromeHeadlessShellInstallable;204const tempDir = Deno.makeTempDirSync();205const ctx = createMockContext(tempDir);206207// Prepare (download + extract)208const pkg = await tool.preparePackage(ctx);209210try {211// Install into real quartoDataDir212await tool.install(pkg, ctx);213214// Verify installed state215assertEquals(await tool.installed(), true);216217const version = await tool.installedVersion();218assert(version, "installedVersion should return a version string");219assert(/^\d+\.\d+\.\d+\.\d+$/.test(version!), `version format: ${version}`);220221const exePath = chromeHeadlessShellExecutablePath();222assert(exePath !== undefined, "executable path should be defined after install");223assert(existsSync(exePath!), `executable should exist at: ${exePath}`);224225const dir = await tool.installDir();226assert(dir !== undefined, "installDir should return a path when installed");227228// Uninstall229await tool.uninstall(ctx);230231// Verify uninstalled state232assertEquals(await tool.installed(), false);233assertEquals(chromeHeadlessShellExecutablePath(), undefined);234} finally {235// Safety net: ensure uninstall happened even if assertions failed236if (await tool.installed()) {237await tool.uninstall(ctx);238}239safeRemoveSync(pkg.filePath, { recursive: true });240safeRemoveSync(tempDir, { recursive: true });241}242}, { ignore: runningInCI() });243244// -- Step 8: Tool registry integration --245246unitTest("tool registry - chrome-headless-shell is listed in installableTools", async () => {247const tools = installableTools();248assert(249tools.includes("chrome-headless-shell"),250`installableTools() should include "chrome-headless-shell", got: ${tools}`,251);252});253254unitTest("tool registry - installableTool looks up chrome-headless-shell", async () => {255const tool = installableTool("chrome-headless-shell");256assert(tool !== undefined, "installableTool should find chrome-headless-shell");257assertEquals(tool.name, "Chrome Headless Shell");258});259260261