Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 120 additions & 16 deletions packages/e2e/tests/app-deploy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,159 @@
import {appTestFixture as test, createApp, deployApp, versionsList} from '../setup/app.js'
import {appTestFixture as test, createApp, deployApp, versionsList, configLink} from '../setup/app.js'
import {teardownAll} from '../setup/teardown.js'
import {TEST_TIMEOUT} from '../setup/constants.js'
import {requireEnv} from '../setup/env.js'
import {expect} from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path' // eslint-disable-line no-restricted-imports
// eslint-disable-next-line no-restricted-imports
import * as path from 'path'

/**
* Test A — full deploy lifecycle (QA checklist: Apps section, deploy flow).
*
* 1. `app init` Create primary app (React Router + JavaScript)
* 2. `app deploy --version v1` Deploy with a version tag
* 3. `app versions list` Verify the primary tag is active and no other
* version is stuck active
* 4. `app config link` from primary dir → creates a brand-new secondary app
* interactively (answers org → "Create new?" → app name; config name
* prompt is skipped via `--config secondary`)
* 5. `app deploy --config secondary` Deploy from primary dir to secondary app
* 6. `app versions list --config secondary` Verify the secondary deploy hit
* the secondary app (not a silent fallback to primary) and the primary
* tag does not leak into secondary's list
*
* Test body is pure CLI; teardown uses the dev dashboard to delete both apps.
*/

interface VersionLine {
versionTag?: string | null
status: string
}

/**
* Asserts a `versions list --json` result shows:
* - `expectedTag` is present and `active`
* - no other version is stuck `active`
* - (if `forbiddenTag` provided) `forbiddenTag` does not appear at all
*
* The last check guards cross-app leakage: a version we expect to live on one
* app should never appear in another app's version list.
*/
function assertActiveVersion(opts: {
result: {stdout: string; stderr: string; exitCode: number}
expectedTag: string
step: string
forbiddenTag?: string
}) {
const {result, expectedTag, step, forbiddenTag} = opts
const output = result.stdout + result.stderr
expect(result.exitCode, `${step} - versions list failed:\n${output}`).toBe(0)
const versions = JSON.parse(result.stdout) as VersionLine[]
const entry = versions.find((version) => version.versionTag === expectedTag)
expect(entry, `${step} - version tag "${expectedTag}" not found in:\n${result.stdout}`).toBeDefined()
expect(entry?.status, `${step} - expected "${expectedTag}" to be active, got "${entry?.status}"`).toBe('active')
const otherActive = versions.filter((version) => version.versionTag !== expectedTag && version.status === 'active')
expect(otherActive, `${step} - unexpected other active versions: ${JSON.stringify(otherActive)}`).toHaveLength(0)
if (forbiddenTag) {
const tags = versions.map((version) => version.versionTag)
expect(tags, `${step} - tag "${forbiddenTag}" unexpectedly found in list`).not.toContain(forbiddenTag)
}
}

test.describe('App deploy', () => {
test('deploy and verify version exists', async ({cli, env, browserPage}) => {
test('init, deploy, versions list, config link, deploy to secondary', async ({cli, env, browserPage}) => {
test.setTimeout(TEST_TIMEOUT.long)
requireEnv(env, 'orgId')

const parentDir = fs.mkdtempSync(path.join(env.tempDir, 'app-'))
const appName = `E2E-deploy-${Date.now()}`
const appName = `E2E-deploy1-${Date.now()}`
const secondaryAppName = `E2E-deploy2-${Date.now()}`

try {
// Step 1: Create an extension-only app (no scopes needed for deploy)
// Step 1: Create primary app (React Router template)
const initResult = await createApp({
cli,
parentDir,
name: appName,
template: 'none',
template: 'reactRouter',
flavor: 'javascript',
packageManager: 'pnpm',
orgId: env.orgId,
})
expect(initResult.exitCode, `createApp failed:\nstdout: ${initResult.stdout}\nstderr: ${initResult.stderr}`).toBe(
0,
)
expect(initResult.exitCode, `Step 1 - primary app init failed:\n${initResult.stderr}`).toBe(0)
const appDir = initResult.appDir

// Step 2: Deploy with a tagged version
const versionTag = `e2e-v-${Date.now()}`
const deployResult = await deployApp({cli, appDir, version: versionTag, message: 'E2E test deployment'})
const versionTag = `E2E-v1-${Date.now()}`
const deployResult = await deployApp({cli, appDir, version: versionTag, message: 'E2E A primary deployment'})
const deployOutput = deployResult.stdout + deployResult.stderr
expect(deployResult.exitCode, `deploy failed:\n${deployOutput}`).toBe(0)
expect(deployResult.exitCode, `Step 2 - deploy failed:\n${deployOutput}`).toBe(0)

// Step 3: Verify the version exists via versions list
// Step 3: Verify the primary tag is active and no other version is stuck active.
const listResult = await versionsList({cli, appDir})
const listOutput = listResult.stdout + listResult.stderr
expect(listResult.exitCode, `versions list failed:\n${listOutput}`).toBe(0)
expect(listOutput).toContain(versionTag)
assertActiveVersion({result: listResult, expectedTag: versionTag, step: 'Step 3'})

// Step 4: Config link from primary dir → creates a brand-new secondary app
// interactively (org → "Create new?" → "App name"). The "Configuration file
// name" prompt is skipped via `--config secondary`.
const secondaryConfig = 'secondary'
const linkResult = await configLink({
cli,
appDir,
appName: secondaryAppName,
orgId: env.orgId,
configName: secondaryConfig,
})
const linkOutput = linkResult.stdout + linkResult.stderr
expect(linkResult.exitCode, `Step 4 - config link failed:\n${linkOutput}`).toBe(0)
expect(linkOutput, `Step 4 - missing "is now linked to \\"${secondaryAppName}\\""`).toContain(
`is now linked to "${secondaryAppName}"`,
)
const secondaryTomlPath = path.join(appDir, `shopify.app.${secondaryConfig}.toml`)
expect(
fs.existsSync(secondaryTomlPath),
`Step 4 - expected ${secondaryTomlPath} to exist after config link`,
).toBe(true)

// Step 5: Deploy from primary dir to secondary app via --config secondary
const secondaryVersionTag = `E2E-v2-${Date.now()}`
const secondaryDeployResult = await deployApp({
cli,
appDir,
config: secondaryConfig,
version: secondaryVersionTag,
message: 'E2E A secondary deployment',
})
const secondaryDeployOutput = secondaryDeployResult.stdout + secondaryDeployResult.stderr
expect(secondaryDeployResult.exitCode, `Step 5 - secondary deploy failed:\n${secondaryDeployOutput}`).toBe(0)

// Step 6: Verify the secondary deploy hit the secondary app (not a silent
// fallback to primary). Checks the secondary tag is active, no other
// version is stuck active, and the primary tag doesn't leak into secondary.
const secondaryListResult = await versionsList({cli, appDir, config: secondaryConfig})
assertActiveVersion({
result: secondaryListResult,
expectedTag: secondaryVersionTag,
step: 'Step 6',
forbiddenTag: versionTag,
})
} finally {
// E2E_SKIP_TEARDOWN=1 skips teardown for debugging. Run cleanup scripts afterward.
if (!process.env.E2E_SKIP_TEARDOWN) {
fs.rmSync(parentDir, {recursive: true, force: true})
// Neither app was installed on a store — delete the apps only (no uninstall)
await teardownAll({
browserPage,
appName,
orgId: env.orgId,
workerIndex: env.workerIndex,
})
await teardownAll({
browserPage,
appName: secondaryAppName,
orgId: env.orgId,
workerIndex: env.workerIndex,
})
}
}
})
Expand Down
Loading