diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 8b9d5de63e8..fa6c33eb21d 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@playwright/test": "^1.50.0", - "@types/node": "18.19.70", +"@types/node": "18.19.70", "execa": "^7.2.0", "node-pty": "^1.0.0", "strip-ansi": "^7.1.0", diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index 5a37752bef3..93f224c1844 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-imports */ import {cliFixture} from './cli.js' import {executables} from './env.js' import {stripAnsi} from '../helpers/strip-ansi.js' @@ -22,6 +21,8 @@ export const authFixture = cliFixture.extend<{}, {authLogin: void}>({ return } + process.stdout.write('[e2e] Authenticating automatically — no action required.\n') + // Clear any existing session await execa('node', [executables.cli, 'auth', 'logout'], { env: env.processEnv, diff --git a/packages/e2e/setup/cli.ts b/packages/e2e/setup/cli.ts index 1f39f38ca68..f3e178abaea 100644 --- a/packages/e2e/setup/cli.ts +++ b/packages/e2e/setup/cli.ts @@ -49,9 +49,16 @@ export const cliFixture = envFixture.extend<{cli: CLIProcess}>({ async exec(args, opts = {}) { // 3 min default const timeout = opts.timeout ?? 3 * 60 * 1000 + const execEnv: {[key: string]: string} = {} + for (const [key, value] of Object.entries({...env.processEnv, ...opts.env})) { + if (value !== undefined) { + execEnv[key] = value + } + } const execaOpts: ExecaOptions = { cwd: opts.cwd, - env: {...env.processEnv, ...opts.env}, + env: execEnv, + extendEnv: false, timeout, reject: false, } @@ -72,9 +79,16 @@ export const cliFixture = envFixture.extend<{cli: CLIProcess}>({ async execCreateApp(args, opts = {}) { // 5 min default for scaffolding const timeout = opts.timeout ?? 5 * 60 * 1000 + const execEnv: {[key: string]: string} = {} + for (const [key, value] of Object.entries({...env.processEnv, ...opts.env})) { + if (value !== undefined) { + execEnv[key] = value + } + } const execaOpts: ExecaOptions = { cwd: opts.cwd, - env: {...env.processEnv, ...opts.env}, + env: execEnv, + extendEnv: false, timeout, reject: false, } diff --git a/packages/e2e/tests/app-basic.spec.ts b/packages/e2e/tests/app-basic.spec.ts new file mode 100644 index 00000000000..0ca04eb118d --- /dev/null +++ b/packages/e2e/tests/app-basic.spec.ts @@ -0,0 +1,129 @@ +/* eslint-disable no-restricted-imports */ +import {appScaffoldFixture as test} from '../setup/app.js' +import {requireEnv} from '../setup/env.js' +import {expect} from '@playwright/test' +import * as fs from 'fs' +import * as path from 'path' + +test.describe('App basic flow (no extensions)', () => { + test('init, dev, execute, quit, clean, deploy, versions, config link, deploy to secondary', async ({ + appScaffold, + cli, + env, + }) => { + // Full flow: init + dev (3 min) + deploy + config link + secondary deploy — needs 10 min + test.setTimeout(10 * 60 * 1000) + + requireEnv(env, 'clientId', 'storeFqdn', 'secondaryClientId') + + // Step 1: Create a React Router app + const initResult = await appScaffold.init({ + template: 'reactRouter', + flavor: 'javascript', + packageManager: 'npm', + }) + expect(initResult.exitCode, '‼️ Step 1 - app init failed').toBe(0) + + // Step 2: Start dev server via PTY + // Unset CI so keyboard shortcuts are enabled in the Dev UI + const dev = await cli.spawn(['app', 'dev', '--path', appScaffold.appDir], {env: {CI: ''}}) + try { + await dev.waitForOutput('Ready, watching for changes in your app', 3 * 60 * 1000).catch((err: Error) => { + throw new Error(`‼️ Step 2 - app dev failed\n${err.message}`) + }) + + // Step 3: Run a GraphQL query while the dev server is running + const executeResult = await cli.exec( + ['app', 'execute', '--query', 'query { shop { name } }', '--path', appScaffold.appDir], + {timeout: 60 * 1000}, + ) + const executeOutput = executeResult.stdout + executeResult.stderr + expect(executeResult.exitCode, '‼️ Step 3 - app execute failed').toBe(0) + expect(executeOutput, '‼️ Step 3 - app execute: response missing "shop" field').toContain('"shop"') + + // Step 4: Press q to quit the dev server + dev.sendKey('q') + const devExitCode = await dev.waitForExit(30_000).catch((err: Error) => { + throw new Error(`‼️ Step 4 - app dev did not exit after pressing q\n${err.message}`) + }) + expect(devExitCode, '‼️ Step 4 - app dev quit failed').toBe(0) + } finally { + // Step 5: Always clean up the dev preview, even if the test fails + dev.kill() + const cleanResult = await cli.exec(['app', 'dev', 'clean', '--path', appScaffold.appDir]) + const cleanOutput = cleanResult.stdout + cleanResult.stderr + expect(cleanResult.exitCode, '‼️ Step 5 - app dev clean failed').toBe(0) + expect(cleanOutput, '‼️ Step 5 - app dev clean: missing "Dev preview stopped" in output').toContain( + 'Dev preview stopped', + ) + } + + // Step 6: Deploy the primary app + const versionTag = `QA-E2E-1st-${Date.now()}` + const deployResult = await cli.exec( + [ + 'app', + 'deploy', + '--path', + appScaffold.appDir, + '--force', + '--version', + versionTag, + '--message', + 'E2E basic flow deployment', + ], + {timeout: 5 * 60 * 1000}, + ) + expect(deployResult.exitCode, '‼️ Step 6 - app deploy failed').toBe(0) + + // Step 7: List versions and verify our tag appears + const listResult = await cli.exec(['app', 'versions', 'list', '--path', appScaffold.appDir, '--json'], { + timeout: 60 * 1000, + }) + const listOutput = listResult.stdout + listResult.stderr + expect(listResult.exitCode, '‼️ Step 7 - app versions list failed').toBe(0) + expect(listOutput, `‼️ Step 7 - app versions list: missing version tag "${versionTag}"`).toContain(versionTag) + + // Step 8: Config link to the secondary app + // Pre-create a minimal TOML stub so getTomls() finds the secondary client ID and skips + // the interactive "Configuration file name" prompt entirely. This avoids PTY timing races + // where the Enter key arrives before ink has fully initialized the text prompt, which + // causes renderTextPrompt to return '' → filenameFromName('') = 'shopify.app.toml' → + // that file already exists → overwrite confirmation prompt hangs. + // (--config and --client-id are mutually exclusive flags, so we can't pass both directly.) + fs.writeFileSync( + path.join(appScaffold.appDir, 'shopify.app.secondary.toml'), + `client_id = "${env.secondaryClientId}"\n`, + ) + + const configLink = await cli.spawn( + ['app', 'config', 'link', '--path', appScaffold.appDir, '--client-id', env.secondaryClientId], + {env: {CI: '', SHOPIFY_FLAG_CLIENT_ID: undefined}}, + ) + await configLink.waitForOutput('is now linked to', 2 * 60 * 1000).catch((err: Error) => { + throw new Error(`‼️ Step 8 - app config link failed\n${err.message}`) + }) + const configLinkExitCode = await configLink.waitForExit(30_000) + expect(configLinkExitCode, '‼️ Step 8 - app config link failed').toBe(0) + + // Step 9: Deploy to the secondary app using the linked config file + const secondaryVersionTag = `QA-E2E-2nd-${Date.now()}` + const secondaryDeployResult = await cli.exec( + [ + 'app', + 'deploy', + '--path', + appScaffold.appDir, + '--config', + 'secondary', + '--force', + '--version', + secondaryVersionTag, + '--message', + 'E2E secondary app deployment', + ], + {timeout: 5 * 60 * 1000, env: {SHOPIFY_FLAG_CLIENT_ID: undefined}}, + ) + expect(secondaryDeployResult.exitCode, '‼️ Step 9 - app deploy (secondary) failed').toBe(0) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c611fe81206..83a7cf0eef1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13842,15 +13842,6 @@ snapshots: msw: 2.12.10(@types/node@22.19.11)(typescript@5.9.3) vite: 6.4.1(@types/node@18.19.70)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) - '@vitest/mocker@3.2.4(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.11)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - msw: 2.12.10(@types/node@22.19.11)(typescript@5.9.3) - vite: 6.4.1(@types/node@22.19.11)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2) - '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -19245,7 +19236,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.11)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(vite@6.4.1(@types/node@18.19.70)(jiti@2.6.1)(sass@1.97.3)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4