1/* eslint-env jest */ 2import { ExpoConfig, getConfig, PackageJSONConfig } from '@expo/config'; 3import JsonFile from '@expo/json-file'; 4import { SpawnOptions, SpawnResult } from '@expo/spawn-async'; 5import execa from 'execa'; 6import fs from 'fs'; 7import os from 'os'; 8import path from 'path'; 9 10import { copySync } from '../../src/utils/dir'; 11 12export const bin = require.resolve('../../build/bin/cli'); 13 14export const projectRoot = getTemporaryPath(); 15 16export function getTemporaryPath() { 17 return path.join(os.tmpdir(), Math.random().toString(36).substring(2)); 18} 19 20export function execute(...args) { 21 return execa('node', [bin, ...args], { cwd: projectRoot }); 22} 23 24export function getRoot(...args) { 25 return path.join(projectRoot, ...args); 26} 27 28export async function abortingSpawnAsync( 29 cmd: string, 30 args: string[], 31 options?: SpawnOptions 32): Promise<SpawnResult> { 33 const spawnAsync = jest.requireActual( 34 '@expo/spawn-async' 35 ) as typeof import('@expo/spawn-async').default; 36 37 const promise = spawnAsync(cmd, args, options); 38 promise.child.stdout.pipe(process.stdout); 39 promise.child.stderr.pipe(process.stderr); 40 41 // TODO: Not sure how to do this yet... 42 // const unsub = addJestInterruptedListener(() => { 43 // promise.child.kill('SIGINT'); 44 // }); 45 try { 46 return await promise; 47 } catch (error) { 48 if (isSpawnResult(error)) { 49 if (error.stdout) error.message += `\n------\nSTDOUT:\n${error.stdout}`; 50 if (error.stderr) error.message += `\n------\nSTDERR:\n${error.stderr}`; 51 } 52 throw error; 53 } finally { 54 // unsub(); 55 } 56} 57 58function isSpawnResult(errorOrResult: Error): errorOrResult is Error & SpawnResult { 59 return 'pid' in errorOrResult && 'stdout' in errorOrResult && 'stderr' in errorOrResult; 60} 61 62export async function installAsync(projectRoot: string, pkgs: string[] = []) { 63 return abortingSpawnAsync('yarn', pkgs, { 64 cwd: projectRoot, 65 stdio: ['ignore', 'pipe', 'pipe'], 66 }); 67} 68 69/** 70 * @param parentDir Directory to create the project folder in, i.e. os temp directory 71 * @param props.dirName Name of the project folder, used to prevent recreating the project locally 72 * @param props.reuseExisting Should reuse the existing project if possible, good for testing locally 73 * @param props.fixtureName Name of the fixture folder to use, this must map to the directories in the `expo/e2e/fixtures/` folder 74 * @param props.config Optional extra values to add inside the app.json `expo` object 75 * @param props.pkg Optional extra values to add to the fixture package.json file before installing 76 * @returns The project root that can be tested inside of 77 */ 78export async function createFromFixtureAsync( 79 parentDir: string, 80 { 81 dirName, 82 reuseExisting, 83 fixtureName, 84 config, 85 pkg, 86 }: { 87 dirName: string; 88 reuseExisting?: boolean; 89 fixtureName: string; 90 config?: Partial<ExpoConfig>; 91 pkg?: Partial<PackageJSONConfig>; 92 } 93): Promise<string> { 94 const projectRoot = path.join(parentDir, dirName); 95 96 if (fs.existsSync(projectRoot)) { 97 if (reuseExisting) { 98 console.log('[setup] Reusing existing fixture project:', projectRoot); 99 // bail out early, this is good for local testing. 100 return projectRoot; 101 } else { 102 console.log('[setup] Clearing existing fixture project:', projectRoot); 103 await fs.promises.rm(projectRoot, { recursive: true, force: true }); 104 } 105 } 106 107 try { 108 const fixturePath = path.join(__dirname, '../fixtures', fixtureName); 109 110 if (!fs.existsSync(fixturePath)) { 111 throw new Error('No fixture project named: ' + fixtureName); 112 } 113 114 // Create the project root 115 fs.mkdirSync(projectRoot, { recursive: true }); 116 console.log('[setup] Created fixture project:', projectRoot); 117 118 // Copy all files recursively into the temporary directory 119 await copySync(fixturePath, projectRoot); 120 121 // Add additional modifications to the package.json 122 if (pkg) { 123 const pkgPath = path.join(projectRoot, 'package.json'); 124 const fixturePkg = (await JsonFile.readAsync(pkgPath)) as PackageJSONConfig; 125 126 await JsonFile.writeAsync(pkgPath, { 127 ...pkg, 128 ...fixturePkg, 129 dependencies: { 130 ...(fixturePkg.dependencies || {}), 131 ...(pkg.dependencies || {}), 132 }, 133 devDependencies: { 134 ...(fixturePkg.devDependencies || {}), 135 ...(pkg.devDependencies || {}), 136 }, 137 scripts: { 138 ...(fixturePkg.scripts || {}), 139 ...(pkg.scripts || {}), 140 }, 141 }); 142 } 143 144 // Add additional modifications to the Expo config 145 if (config) { 146 const { rootConfig, staticConfigPath } = getConfig(projectRoot, { 147 // pkgs not installed yet 148 skipSDKVersionRequirement: true, 149 skipPlugins: true, 150 }); 151 152 const modifiedConfig = { 153 ...rootConfig, 154 expo: { 155 ...(rootConfig.expo || {}), 156 ...config, 157 }, 158 }; 159 await JsonFile.writeAsync(staticConfigPath, modifiedConfig as any); 160 } 161 162 // Install the packages for e2e experience. 163 await installAsync(projectRoot); 164 } catch (error) { 165 // clean up if something failed. 166 // await fs.remove(projectRoot).catch(() => null); 167 throw error; 168 } 169 170 return projectRoot; 171} 172 173// Set this to true to enable caching and prevent rerunning yarn installs 174const testingLocally = !process.env.CI; 175 176export async function setupTestProjectAsync(name: string, fixtureName: string): Promise<string> { 177 // If you're testing this locally, you can set the projectRoot to a local project (you created with expo init) to save time. 178 const projectRoot = await createFromFixtureAsync(os.tmpdir(), { 179 dirName: name, 180 reuseExisting: testingLocally, 181 fixtureName, 182 }); 183 184 // Many of the factors in this test are based on the expected SDK version that we're testing against. 185 const { exp } = getConfig(projectRoot, { skipPlugins: true }); 186 expect(exp.sdkVersion).toBe('45.0.0'); 187 return projectRoot; 188} 189 190/** Returns a list of loaded modules relative to the repo root. Useful for preventing lazy loading from breaking unexpectedly. */ 191export async function getLoadedModulesAsync(statement: string): Promise<string[]> { 192 const repoRoot = path.join(__dirname, '../../../../'); 193 const results = await execa( 194 'node', 195 [ 196 '-e', 197 [statement, `console.log(JSON.stringify(Object.keys(require('module')._cache)));`].join('\n'), 198 ], 199 { cwd: __dirname } 200 ); 201 const loadedModules = JSON.parse(results.stdout.trim()); 202 return loadedModules.map((value) => path.relative(repoRoot, value)).sort(); 203} 204