import { dirname, join, relative, resolve } from 'path'; import fs from 'fs-extra'; import glob from 'fast-glob'; import { execaCommand } from 'execa'; import { InlineConfig, UserConfig, build } from '../src'; import { normalizePath } from '../src/core/utils/paths'; import merge from 'lodash.merge'; export class TestProject { files: Array<[string, string]> = []; config: UserConfig | undefined; readonly root: string; constructor(packageJson: any = {}) { // We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts // file is cached and cannot be different between each test. Instead, we add a random ID to the // end to make each test's path unique. const id = Math.random().toString(32).substring(3); this.root = join('e2e/dist', id); this.files.push([ 'package.json', JSON.stringify( merge( { name: 'E2E Extension', description: 'Example description', version: '0.0.0', dependencies: { wxt: '../../..', }, }, packageJson, ), null, 2, ), ]); } /** * Add a `wxt.config.ts` to the project with specific contents. */ setConfigFileConfig(config: UserConfig = {}) { this.config = config; this.files.push([ 'wxt.config.ts', `import { defineConfig } from 'wxt'\n\nexport default defineConfig(${JSON.stringify( config, null, 2, )})`, ]); } /** * Adds the file to the project. Stored in memory until `.build` is called. * * @param filename Filename relative to the project's root. * @param content File content. */ addFile(filename: string, content?: string) { this.files.push([filename, content ?? '']); if (filename === 'wxt.config.ts') this.config = {}; } /** * Write the files to the test directory install dependencies, and build the project. */ async build(config: InlineConfig = {}) { if (this.config == null) this.setConfigFileConfig(); for (const file of this.files) { const [name, content] = file; const filePath = resolve(this.root, name); const fileDir = dirname(filePath); await fs.ensureDir(fileDir); await fs.writeFile(filePath, content ?? '', 'utf-8'); } await execaCommand('npm i --ignore-scripts', { cwd: this.root }); await build({ ...config, root: this.root }); } /** * Read all the files from the test project's `.output` directory and combine them into a string * that can be used in a snapshot. * * Optionally, provide a list of filenames whose content is not printed (because it's inconsistent * or not relevant to a test). */ serializeOutput(ignoreContentsOfFilenames?: string[]): Promise { return this.serializeDir('.output', ignoreContentsOfFilenames); } /** * Read all the files from the test project's `.wxt` directory and combine them into a string * that can be used in a snapshot. */ serializeWxtDir(): Promise { return this.serializeDir( resolve(this.config?.srcDir ?? this.root, '.wxt/types'), ); } /** * Deeply print the filename and contents of all files in a directory. * * Optionally, provide a list of filenames whose content is not printed (because it's inconsistent * or not relevant to a test). */ private async serializeDir( dir: string, ignoreContentsOfFilenames?: string[], ): Promise { const outputFiles = await glob('**/*', { cwd: resolve(this.root, dir), ignore: ['**/node_modules', '**/.output'], }); outputFiles.sort(); const fileContents = []; for (const file of outputFiles) { const path = resolve(this.root, dir, file); const isContentIgnored = !!ignoreContentsOfFilenames?.find( (ignoredFile) => normalizePath(path).endsWith(ignoredFile), ); fileContents.push(await this.serializeFile(path, isContentIgnored)); } return fileContents.join(`\n${''.padEnd(80, '=')}\n`); } /** * @param path An abosolute path to a file or a path relative to the root. * @param ignoreContents An optional boolean that, when true, causes this function to not print * the file contents. */ async serializeFile(path: string, ignoreContents?: boolean): Promise { const absolutePath = resolve(this.root, path); return [ normalizePath(relative(this.root, absolutePath)), ignoreContents ? '' : await fs.readFile(absolutePath), ].join(`\n${''.padEnd(40, '-')}\n`); } fileExists(path: string): Promise { return fs.exists(resolve(this.root, path)); } async getOutputManifest( path: string = '.output/chrome-mv3/manifest.json', ): Promise { return await fs.readJson(resolve(this.root, path)); } }