UNPKG

5.82 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
2// Node module: @loopback/testlab
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 appendFile,
8 copy,
9 emptyDir,
10 ensureDir,
11 ensureDirSync,
12 mkdtempSync,
13 outputFile,
14 outputJson,
15 pathExists,
16 readFile,
17 remove,
18} from 'fs-extra';
19import {join, parse, resolve} from 'path';
20
21/**
22 * Options for a test sandbox
23 */
24export interface TestSandboxOptions {
25 /**
26 * The `subdir` controls if/how the sandbox creates a subdirectory under the
27 * root path. It has one of the following values:
28 *
29 * - `true`: Creates a unique subdirectory. This will be the default behavior.
30 * - `false`: Uses the root path as the target directory without creating a
31 * subdirectory.
32 * - a string such as `sub-dir-1`: creates a subdirectory with the given value.
33 */
34 subdir: boolean | string;
35}
36
37/**
38 * TestSandbox class provides a convenient way to get a reference to a
39 * sandbox folder in which you can perform operations for testing purposes.
40 */
41export class TestSandbox {
42 // Path of the TestSandbox
43 private _path?: string;
44
45 public get path(): string {
46 if (!this._path) {
47 throw new Error(
48 `TestSandbox instance was deleted. Create a new instance.`,
49 );
50 }
51 return this._path;
52 }
53
54 /**
55 * Will create a directory if it doesn't already exist. If it exists, you
56 * still get an instance of the TestSandbox.
57 *
58 * @example
59 * ```ts
60 * // Create a sandbox as a unique temporary subdirectory under the rootPath
61 * const sandbox = new TestSandbox(rootPath);
62 * const sandbox = new TestSandbox(rootPath, {subdir: true});
63 *
64 * // Create a sandbox in the root path directly
65 * // This is same as the old behavior
66 * const sandbox = new TestSandbox(rootPath, {subdir: false});
67 *
68 * // Create a sandbox in the `test1` subdirectory of the root path
69 * const sandbox = new TestSandbox(rootPath, {subdir: 'test1'});
70 * ```
71 *
72 * @param rootPath - Root path of the TestSandbox. If relative it will be
73 * resolved against the current directory.
74 * @param options - Options to control if/how the sandbox creates a
75 * subdirectory for the sandbox. If not provided, the sandbox
76 * will automatically creates a unique temporary subdirectory. This allows
77 * sandboxes with the same root path can be used in parallel during testing.
78 */
79 constructor(rootPath: string, options?: TestSandboxOptions) {
80 rootPath = resolve(rootPath);
81 ensureDirSync(rootPath);
82 options = {subdir: true, ...options};
83 const subdir = typeof options.subdir === 'string' ? options.subdir : '.';
84 if (options.subdir !== true) {
85 this._path = resolve(rootPath, subdir);
86 } else {
87 // Create a unique temporary directory under the root path
88 // See https://nodejs.org/api/fs.html#fs_fs_mkdtempsync_prefix_options
89 this._path = mkdtempSync(join(rootPath, `/${process.pid}`));
90 }
91 }
92
93 /**
94 * Resets the TestSandbox. (Remove all files in it).
95 */
96 async reset(): Promise<void> {
97 // Decache files from require's cache so future tests aren't affected incase
98 // a file is recreated in sandbox with the same file name but different
99 // contents after resetting the sandbox.
100 for (const key in require.cache) {
101 if (key.startsWith(this.path)) {
102 delete require.cache[key];
103 }
104 }
105
106 await emptyDir(this.path);
107 }
108
109 /**
110 * Deletes the TestSandbox.
111 */
112 async delete(): Promise<void> {
113 await remove(this.path);
114 delete this._path;
115 }
116
117 /**
118 * Makes a directory in the TestSandbox
119 *
120 * @param dir - Name of directory to create (relative to TestSandbox path)
121 */
122 async mkdir(dir: string): Promise<void> {
123 await ensureDir(resolve(this.path, dir));
124 }
125
126 /**
127 * Copies a file from src to the TestSandbox. If copying a `.js` file which
128 * has an accompanying `.js.map` file in the src file location, the dest file
129 * will have its sourceMappingURL updated to point to the original file as
130 * an absolute path so you don't need to copy the map file.
131 *
132 * @param src - Absolute path of file to be copied to the TestSandbox
133 * @param dest - Optional. Destination filename of the copy operation
134 * (relative to TestSandbox). Original filename used if not specified.
135 * @param transform - Optional. A function to transform the file content.
136 */
137 async copyFile(
138 src: string,
139 dest?: string,
140 transform?: (content: string) => string,
141 ): Promise<void> {
142 dest = dest
143 ? resolve(this.path, dest)
144 : resolve(this.path, parse(src).base);
145
146 if (transform == null) {
147 await copy(src, dest);
148 } else {
149 let content = await readFile(src, 'utf-8');
150 content = transform(content);
151 await outputFile(dest, content, {encoding: 'utf-8'});
152 }
153
154 if (parse(src).ext === '.js' && (await pathExists(src + '.map'))) {
155 const srcMap = src + '.map';
156 await appendFile(dest, `\n//# sourceMappingURL=${srcMap}`);
157 }
158 }
159
160 /**
161 * Creates a new file and writes the given data serialized as JSON.
162 *
163 * @param dest - Destination filename, optionally including a relative path.
164 * @param data - The data to write.
165 */
166 async writeJsonFile(dest: string, data: unknown): Promise<void> {
167 dest = resolve(this.path, dest);
168 return outputJson(dest, data, {spaces: 2});
169 }
170
171 /**
172 * Creates a new file and writes the given data as a UTF-8-encoded text.
173 *
174 * @param dest - Destination filename, optionally including a relative path.
175 * @param data - The text to write.
176 */
177 async writeTextFile(dest: string, data: string): Promise<void> {
178 dest = resolve(this.path, dest);
179 return outputFile(dest, data, 'utf-8');
180 }
181}