188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
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,
|
|
createServer,
|
|
prepare,
|
|
zip,
|
|
} 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 = {};
|
|
}
|
|
|
|
async prepare(config: InlineConfig = {}) {
|
|
await this.writeProjectToDisk();
|
|
await prepare({ ...config, root: this.root });
|
|
}
|
|
|
|
async build(config: InlineConfig = {}) {
|
|
await this.writeProjectToDisk();
|
|
await build({ ...config, root: this.root });
|
|
}
|
|
|
|
async zip(config: InlineConfig = {}) {
|
|
await this.writeProjectToDisk();
|
|
await zip({ ...config, root: this.root });
|
|
}
|
|
|
|
async startServer(config: InlineConfig = {}) {
|
|
await this.writeProjectToDisk();
|
|
const server = await createServer({ ...config, root: this.root });
|
|
await server.start();
|
|
return server;
|
|
}
|
|
|
|
/**
|
|
* Call `path.resolve` relative to the project's root directory.
|
|
*/
|
|
resolvePath(...path: string[]): string {
|
|
return resolve(this.root, ...path);
|
|
}
|
|
|
|
private async writeProjectToDisk() {
|
|
if (this.config == null) this.setConfigFileConfig();
|
|
|
|
for (const file of this.files) {
|
|
const [name, content] = file;
|
|
const filePath = this.resolvePath(name);
|
|
const fileDir = dirname(filePath);
|
|
await fs.ensureDir(fileDir);
|
|
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
|
}
|
|
|
|
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
|
|
cwd: 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<string> {
|
|
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<string> {
|
|
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<string> {
|
|
const outputFiles = await glob('**/*', {
|
|
cwd: this.resolvePath(dir),
|
|
ignore: ['**/node_modules', '**/.output'],
|
|
});
|
|
outputFiles.sort();
|
|
const fileContents = [];
|
|
for (const file of outputFiles) {
|
|
const path = this.resolvePath(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 absolute 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<string> {
|
|
const absolutePath = this.resolvePath(path);
|
|
return [
|
|
normalizePath(relative(this.root, absolutePath)),
|
|
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
|
|
].join(`\n${''.padEnd(40, '-')}\n`);
|
|
}
|
|
|
|
fileExists(...path: string[]): Promise<boolean> {
|
|
return fs.exists(this.resolvePath(...path));
|
|
}
|
|
|
|
async getOutputManifest(
|
|
path: string = '.output/chrome-mv3/manifest.json',
|
|
): Promise<any> {
|
|
return await fs.readJson(this.resolvePath(path));
|
|
}
|
|
}
|