|
1 | | -import {appTestFixture as test, createApp, deployApp, versionsList} from '../setup/app.js' |
| 1 | +import {appTestFixture as test, createApp, deployApp, versionsList, configLink} from '../setup/app.js' |
2 | 2 | import {teardownAll} from '../setup/teardown.js' |
3 | 3 | import {TEST_TIMEOUT} from '../setup/constants.js' |
4 | 4 | import {requireEnv} from '../setup/env.js' |
5 | 5 | import {expect} from '@playwright/test' |
6 | 6 | import * as fs from 'fs' |
7 | | -import * as path from 'path' // eslint-disable-line no-restricted-imports |
| 7 | +// eslint-disable-next-line no-restricted-imports |
| 8 | +import * as path from 'path' |
| 9 | + |
| 10 | +/** |
| 11 | + * Test A — full deploy lifecycle (QA checklist: Apps section, deploy flow). |
| 12 | + * |
| 13 | + * 1. `app init` Create primary app (React Router + JavaScript) |
| 14 | + * 2. `app deploy --version v1` Deploy with a version tag |
| 15 | + * 3. `app versions list` Verify the primary tag is active and no other |
| 16 | + * version is stuck active |
| 17 | + * 4. `app config link` from primary dir → creates a brand-new secondary app |
| 18 | + * interactively (answers org → "Create new?" → app name; config name |
| 19 | + * prompt is skipped via `--config secondary`) |
| 20 | + * 5. `app deploy --config secondary` Deploy from primary dir to secondary app |
| 21 | + * 6. `app versions list --config secondary` Verify the secondary deploy hit |
| 22 | + * the secondary app (not a silent fallback to primary) and the primary |
| 23 | + * tag does not leak into secondary's list |
| 24 | + * |
| 25 | + * Test body is pure CLI; teardown uses the dev dashboard to delete both apps. |
| 26 | + */ |
| 27 | + |
| 28 | +interface VersionLine { |
| 29 | + versionTag?: string | null |
| 30 | + status: string |
| 31 | +} |
| 32 | + |
| 33 | +/** |
| 34 | + * Asserts a `versions list --json` result shows: |
| 35 | + * - `expectedTag` is present and `active` |
| 36 | + * - no other version is stuck `active` |
| 37 | + * - (if `forbiddenTag` provided) `forbiddenTag` does not appear at all |
| 38 | + * |
| 39 | + * The last check guards cross-app leakage: a version we expect to live on one |
| 40 | + * app should never appear in another app's version list. |
| 41 | + */ |
| 42 | +function assertActiveVersion(opts: { |
| 43 | + result: {stdout: string; stderr: string; exitCode: number} |
| 44 | + expectedTag: string |
| 45 | + step: string |
| 46 | + forbiddenTag?: string |
| 47 | +}) { |
| 48 | + const {result, expectedTag, step, forbiddenTag} = opts |
| 49 | + const output = result.stdout + result.stderr |
| 50 | + expect(result.exitCode, `${step} - versions list failed:\n${output}`).toBe(0) |
| 51 | + const versions = JSON.parse(result.stdout) as VersionLine[] |
| 52 | + const entry = versions.find((version) => version.versionTag === expectedTag) |
| 53 | + expect(entry, `${step} - version tag "${expectedTag}" not found in:\n${result.stdout}`).toBeDefined() |
| 54 | + expect(entry?.status, `${step} - expected "${expectedTag}" to be active, got "${entry?.status}"`).toBe('active') |
| 55 | + const otherActive = versions.filter((version) => version.versionTag !== expectedTag && version.status === 'active') |
| 56 | + expect(otherActive, `${step} - unexpected other active versions: ${JSON.stringify(otherActive)}`).toHaveLength(0) |
| 57 | + if (forbiddenTag) { |
| 58 | + const tags = versions.map((version) => version.versionTag) |
| 59 | + expect(tags, `${step} - tag "${forbiddenTag}" unexpectedly found in list`).not.toContain(forbiddenTag) |
| 60 | + } |
| 61 | +} |
8 | 62 |
|
9 | 63 | test.describe('App deploy', () => { |
10 | | - test('deploy and verify version exists', async ({cli, env, browserPage}) => { |
| 64 | + test('init, deploy, versions list, config link, deploy to secondary', async ({cli, env, browserPage}) => { |
11 | 65 | test.setTimeout(TEST_TIMEOUT.long) |
12 | 66 | requireEnv(env, 'orgId') |
13 | 67 |
|
14 | 68 | const parentDir = fs.mkdtempSync(path.join(env.tempDir, 'app-')) |
15 | | - const appName = `E2E-deploy-${Date.now()}` |
| 69 | + const appName = `E2E-deploy1-${Date.now()}` |
| 70 | + const secondaryAppName = `E2E-deploy2-${Date.now()}` |
16 | 71 |
|
17 | 72 | try { |
18 | | - // Step 1: Create an extension-only app (no scopes needed for deploy) |
| 73 | + // Step 1: Create primary app (React Router template) |
19 | 74 | const initResult = await createApp({ |
20 | 75 | cli, |
21 | 76 | parentDir, |
22 | 77 | name: appName, |
23 | | - template: 'none', |
| 78 | + template: 'reactRouter', |
| 79 | + flavor: 'javascript', |
24 | 80 | packageManager: 'pnpm', |
25 | 81 | orgId: env.orgId, |
26 | 82 | }) |
27 | | - expect(initResult.exitCode, `createApp failed:\nstdout: ${initResult.stdout}\nstderr: ${initResult.stderr}`).toBe( |
28 | | - 0, |
29 | | - ) |
| 83 | + expect(initResult.exitCode, `Step 1 - primary app init failed:\n${initResult.stderr}`).toBe(0) |
30 | 84 | const appDir = initResult.appDir |
31 | 85 |
|
32 | 86 | // Step 2: Deploy with a tagged version |
33 | | - const versionTag = `e2e-v-${Date.now()}` |
34 | | - const deployResult = await deployApp({cli, appDir, version: versionTag, message: 'E2E test deployment'}) |
| 87 | + const versionTag = `E2E-v1-${Date.now()}` |
| 88 | + const deployResult = await deployApp({cli, appDir, version: versionTag, message: 'E2E A primary deployment'}) |
35 | 89 | const deployOutput = deployResult.stdout + deployResult.stderr |
36 | | - expect(deployResult.exitCode, `deploy failed:\n${deployOutput}`).toBe(0) |
| 90 | + expect(deployResult.exitCode, `Step 2 - deploy failed:\n${deployOutput}`).toBe(0) |
37 | 91 |
|
38 | | - // Step 3: Verify the version exists via versions list |
| 92 | + // Step 3: Verify the primary tag is active and no other version is stuck active. |
39 | 93 | const listResult = await versionsList({cli, appDir}) |
40 | | - const listOutput = listResult.stdout + listResult.stderr |
41 | | - expect(listResult.exitCode, `versions list failed:\n${listOutput}`).toBe(0) |
42 | | - expect(listOutput).toContain(versionTag) |
| 94 | + assertActiveVersion({result: listResult, expectedTag: versionTag, step: 'Step 3'}) |
| 95 | + |
| 96 | + // Step 4: Config link from primary dir → creates a brand-new secondary app |
| 97 | + // interactively (org → "Create new?" → "App name"). The "Configuration file |
| 98 | + // name" prompt is skipped via `--config secondary`. |
| 99 | + const secondaryConfig = 'secondary' |
| 100 | + const linkResult = await configLink({ |
| 101 | + cli, |
| 102 | + appDir, |
| 103 | + appName: secondaryAppName, |
| 104 | + orgId: env.orgId, |
| 105 | + configName: secondaryConfig, |
| 106 | + }) |
| 107 | + const linkOutput = linkResult.stdout + linkResult.stderr |
| 108 | + expect(linkResult.exitCode, `Step 4 - config link failed:\n${linkOutput}`).toBe(0) |
| 109 | + expect(linkOutput, `Step 4 - missing "is now linked to \\"${secondaryAppName}\\""`).toContain( |
| 110 | + `is now linked to "${secondaryAppName}"`, |
| 111 | + ) |
| 112 | + const secondaryTomlPath = path.join(appDir, `shopify.app.${secondaryConfig}.toml`) |
| 113 | + expect( |
| 114 | + fs.existsSync(secondaryTomlPath), |
| 115 | + `Step 4 - expected ${secondaryTomlPath} to exist after config link`, |
| 116 | + ).toBe(true) |
| 117 | + |
| 118 | + // Step 5: Deploy from primary dir to secondary app via --config secondary |
| 119 | + const secondaryVersionTag = `E2E-v2-${Date.now()}` |
| 120 | + const secondaryDeployResult = await deployApp({ |
| 121 | + cli, |
| 122 | + appDir, |
| 123 | + config: secondaryConfig, |
| 124 | + version: secondaryVersionTag, |
| 125 | + message: 'E2E A secondary deployment', |
| 126 | + }) |
| 127 | + const secondaryDeployOutput = secondaryDeployResult.stdout + secondaryDeployResult.stderr |
| 128 | + expect(secondaryDeployResult.exitCode, `Step 5 - secondary deploy failed:\n${secondaryDeployOutput}`).toBe(0) |
| 129 | + |
| 130 | + // Step 6: Verify the secondary deploy hit the secondary app (not a silent |
| 131 | + // fallback to primary). Checks the secondary tag is active, no other |
| 132 | + // version is stuck active, and the primary tag doesn't leak into secondary. |
| 133 | + const secondaryListResult = await versionsList({cli, appDir, config: secondaryConfig}) |
| 134 | + assertActiveVersion({ |
| 135 | + result: secondaryListResult, |
| 136 | + expectedTag: secondaryVersionTag, |
| 137 | + step: 'Step 6', |
| 138 | + forbiddenTag: versionTag, |
| 139 | + }) |
43 | 140 | } finally { |
44 | 141 | // E2E_SKIP_TEARDOWN=1 skips teardown for debugging. Run cleanup scripts afterward. |
45 | 142 | if (!process.env.E2E_SKIP_TEARDOWN) { |
46 | 143 | fs.rmSync(parentDir, {recursive: true, force: true}) |
| 144 | + // Neither app was installed on a store — delete the apps only (no uninstall) |
47 | 145 | await teardownAll({ |
48 | 146 | browserPage, |
49 | 147 | appName, |
50 | 148 | orgId: env.orgId, |
51 | 149 | workerIndex: env.workerIndex, |
52 | 150 | }) |
| 151 | + await teardownAll({ |
| 152 | + browserPage, |
| 153 | + appName: secondaryAppName, |
| 154 | + orgId: env.orgId, |
| 155 | + workerIndex: env.workerIndex, |
| 156 | + }) |
53 | 157 | } |
54 | 158 | } |
55 | 159 | }) |
|
0 commit comments