|
2 | 2 | import {authFixture} from './auth.js' |
3 | 3 | import * as path from 'path' |
4 | 4 | import * as fs from 'fs' |
5 | | -import type {ExecResult} from './cli.js' |
| 5 | +import type {CLIProcess, ExecResult} from './cli.js' |
6 | 6 |
|
7 | 7 | export interface AppScaffold { |
8 | 8 | /** The directory where the app was created */ |
@@ -40,97 +40,124 @@ export interface AppInfoResult { |
40 | 40 | }[] |
41 | 41 | } |
42 | 42 |
|
43 | | -/** |
44 | | - * Test-scoped fixture that creates a fresh app in a temp directory. |
45 | | - * Depends on authLogin (worker-scoped) for OAuth session. |
46 | | - */ |
47 | | -export const appScaffoldFixture = authFixture.extend<{appScaffold: AppScaffold}>({ |
48 | | - appScaffold: async ({cli, env, authLogin: _authLogin}, use) => { |
49 | | - const appTmpDir = fs.mkdtempSync(path.join(env.tempDir, 'app-')) |
50 | | - let appDir = '' |
51 | | - |
52 | | - const scaffold: AppScaffold = { |
53 | | - get appDir() { |
54 | | - if (!appDir) throw new Error('App has not been initialized yet. Call init() first.') |
55 | | - return appDir |
56 | | - }, |
57 | | - |
58 | | - async init(opts: AppInitOptions) { |
59 | | - const name = opts.name ?? 'e2e-test-app' |
60 | | - const template = opts.template ?? 'reactRouter' |
61 | | - const packageManager = opts.packageManager ?? 'npm' |
62 | | - |
63 | | - const args = [ |
64 | | - '--name', |
65 | | - name, |
66 | | - '--path', |
67 | | - appTmpDir, |
68 | | - '--package-manager', |
69 | | - packageManager, |
70 | | - '--local', |
71 | | - '--template', |
72 | | - template, |
73 | | - ] |
74 | | - if (opts.flavor) args.push('--flavor', opts.flavor) |
75 | | - |
76 | | - const result = await cli.execCreateApp(args, { |
77 | | - env: {FORCE_COLOR: '0'}, |
78 | | - timeout: 5 * 60 * 1000, |
79 | | - }) |
80 | | - |
81 | | - const allOutput = `${result.stdout}\n${result.stderr}` |
82 | | - const match = allOutput.match(/([\w-]+) is ready for you to build!/) |
83 | | - |
84 | | - if (match?.[1]) { |
85 | | - appDir = path.join(appTmpDir, match[1]) |
| 43 | +/** Shared scaffold builder. defaultName is used when opts.name is omitted. */ |
| 44 | +function buildScaffold( |
| 45 | + cli: CLIProcess, |
| 46 | + appTmpDir: string, |
| 47 | + defaultName: string, |
| 48 | + orgId?: string, |
| 49 | +): {scaffold: AppScaffold} { |
| 50 | + let appDir = '' |
| 51 | + |
| 52 | + const scaffold: AppScaffold = { |
| 53 | + get appDir() { |
| 54 | + if (!appDir) throw new Error('App has not been initialized yet. Call init() first.') |
| 55 | + return appDir |
| 56 | + }, |
| 57 | + |
| 58 | + async init(opts: AppInitOptions) { |
| 59 | + const name = opts.name ?? defaultName |
| 60 | + const template = opts.template ?? 'reactRouter' |
| 61 | + const packageManager = opts.packageManager ?? 'npm' |
| 62 | + |
| 63 | + const args = [ |
| 64 | + '--name', |
| 65 | + name, |
| 66 | + '--path', |
| 67 | + appTmpDir, |
| 68 | + '--package-manager', |
| 69 | + packageManager, |
| 70 | + '--local', |
| 71 | + '--template', |
| 72 | + template, |
| 73 | + ] |
| 74 | + if (orgId) args.push('--organization-id', orgId) |
| 75 | + if (opts.flavor) args.push('--flavor', opts.flavor) |
| 76 | + |
| 77 | + const result = await cli.execCreateApp(args, { |
| 78 | + env: {FORCE_COLOR: '0'}, |
| 79 | + timeout: 5 * 60 * 1000, |
| 80 | + }) |
| 81 | + |
| 82 | + if (result.exitCode !== 0) { |
| 83 | + return result |
| 84 | + } |
| 85 | + |
| 86 | + const allOutput = `${result.stdout}\n${result.stderr}` |
| 87 | + const match = allOutput.match(/([\w-]+) is ready for you to build!/) |
| 88 | + |
| 89 | + if (match?.[1]) { |
| 90 | + appDir = path.join(appTmpDir, match[1]) |
| 91 | + } else { |
| 92 | + const entries = fs.readdirSync(appTmpDir, {withFileTypes: true}) |
| 93 | + const appEntry = entries.find( |
| 94 | + (entry) => entry.isDirectory() && fs.existsSync(path.join(appTmpDir, entry.name, 'shopify.app.toml')), |
| 95 | + ) |
| 96 | + if (appEntry) { |
| 97 | + appDir = path.join(appTmpDir, appEntry.name) |
86 | 98 | } else { |
87 | | - const entries = fs.readdirSync(appTmpDir, {withFileTypes: true}) |
88 | | - const appEntry = entries.find( |
89 | | - (entry) => entry.isDirectory() && fs.existsSync(path.join(appTmpDir, entry.name, 'shopify.app.toml')), |
| 99 | + throw new Error( |
| 100 | + `Could not find created app directory in ${appTmpDir}.\n` + |
| 101 | + `Exit code: ${result.exitCode}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, |
90 | 102 | ) |
91 | | - if (appEntry) { |
92 | | - appDir = path.join(appTmpDir, appEntry.name) |
93 | | - } else { |
94 | | - throw new Error( |
95 | | - `Could not find created app directory in ${appTmpDir}.\n` + |
96 | | - `Exit code: ${result.exitCode}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, |
97 | | - ) |
98 | | - } |
99 | 103 | } |
| 104 | + } |
100 | 105 |
|
101 | | - const npmrcPath = path.join(appDir, '.npmrc') |
102 | | - if (!fs.existsSync(npmrcPath)) fs.writeFileSync(npmrcPath, '') |
103 | | - fs.appendFileSync(npmrcPath, 'frozen-lockfile=false\n') |
| 106 | + const npmrcPath = path.join(appDir, '.npmrc') |
| 107 | + if (!fs.existsSync(npmrcPath)) fs.writeFileSync(npmrcPath, '') |
| 108 | + fs.appendFileSync(npmrcPath, 'frozen-lockfile=false\n') |
104 | 109 |
|
105 | | - return result |
106 | | - }, |
107 | | - |
108 | | - async generateExtension(opts: ExtensionOptions) { |
109 | | - const args = [ |
110 | | - 'app', |
111 | | - 'generate', |
112 | | - 'extension', |
113 | | - '--name', |
114 | | - opts.name, |
115 | | - '--path', |
116 | | - appDir, |
117 | | - '--template', |
118 | | - opts.template, |
119 | | - ] |
120 | | - if (opts.flavor) args.push('--flavor', opts.flavor) |
121 | | - return cli.exec(args, {timeout: 5 * 60 * 1000}) |
122 | | - }, |
123 | | - |
124 | | - async build() { |
125 | | - return cli.exec(['app', 'build', '--path', appDir], {timeout: 5 * 60 * 1000}) |
126 | | - }, |
127 | | - |
128 | | - async appInfo(): Promise<AppInfoResult> { |
129 | | - const result = await cli.exec(['app', 'info', '--path', appDir, '--json']) |
130 | | - return JSON.parse(result.stdout) |
131 | | - }, |
132 | | - } |
| 110 | + return result |
| 111 | + }, |
| 112 | + |
| 113 | + async generateExtension(opts: ExtensionOptions) { |
| 114 | + const args = ['app', 'generate', 'extension', '--name', opts.name, '--path', appDir, '--template', opts.template] |
| 115 | + if (opts.flavor) args.push('--flavor', opts.flavor) |
| 116 | + return cli.exec(args, {timeout: 5 * 60 * 1000}) |
| 117 | + }, |
133 | 118 |
|
| 119 | + async build() { |
| 120 | + return cli.exec(['app', 'build', '--path', appDir], {timeout: 5 * 60 * 1000}) |
| 121 | + }, |
| 122 | + |
| 123 | + async appInfo(): Promise<AppInfoResult> { |
| 124 | + const result = await cli.exec(['app', 'info', '--path', appDir, '--json']) |
| 125 | + return JSON.parse(result.stdout) |
| 126 | + }, |
| 127 | + } |
| 128 | + |
| 129 | + return {scaffold} |
| 130 | +} |
| 131 | + |
| 132 | +/** Fixture: scaffolds a local app linked to a pre-existing remote app (via SHOPIFY_FLAG_CLIENT_ID). */ |
| 133 | +export const appScaffoldFixture = authFixture.extend<{appScaffold: AppScaffold}>({ |
| 134 | + appScaffold: async ({cli, env, authLogin: _authLogin}, use) => { |
| 135 | + const appTmpDir = fs.mkdtempSync(path.join(env.tempDir, 'app-')) |
| 136 | + const {scaffold} = buildScaffold(cli, appTmpDir, 'e2e-test-app') |
| 137 | + await use(scaffold) |
| 138 | + fs.rmSync(appTmpDir, {recursive: true, force: true}) |
| 139 | + }, |
| 140 | +}) |
| 141 | + |
| 142 | +/** CLI wrapper that strips SHOPIFY_FLAG_CLIENT_ID so commands use the toml's client_id. */ |
| 143 | +function makeFreshCli(baseCli: CLIProcess, baseProcessEnv: NodeJS.ProcessEnv): CLIProcess { |
| 144 | + const freshEnv = {...baseProcessEnv, SHOPIFY_FLAG_CLIENT_ID: undefined} |
| 145 | + return { |
| 146 | + exec: (args, opts = {}) => baseCli.exec(args, {...opts, env: {...freshEnv, ...opts.env}}), |
| 147 | + execCreateApp: (args, opts = {}) => baseCli.execCreateApp(args, {...opts, env: {...freshEnv, ...opts.env}}), |
| 148 | + spawn: (args, opts = {}) => baseCli.spawn(args, {...opts, env: {...freshEnv, ...opts.env}}), |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +/** Fixture: creates a brand-new app on every run. Requires E2E_ORG_ID. */ |
| 153 | +export const freshAppScaffoldFixture = authFixture.extend<{appScaffold: AppScaffold; cli: CLIProcess}>({ |
| 154 | + cli: async ({cli: baseCli, env}, use) => { |
| 155 | + await use(makeFreshCli(baseCli, env.processEnv)) |
| 156 | + }, |
| 157 | + |
| 158 | + appScaffold: async ({cli, env, authLogin: _authLogin}, use) => { |
| 159 | + const appTmpDir = fs.mkdtempSync(path.join(env.tempDir, 'fresh-app-')) |
| 160 | + const {scaffold} = buildScaffold(cli, appTmpDir, `QA-E2E-1st-${Date.now()}`, env.orgId || undefined) |
134 | 161 | await use(scaffold) |
135 | 162 | fs.rmSync(appTmpDir, {recursive: true, force: true}) |
136 | 163 | }, |
|
0 commit comments