Skip to content
Open
Show file tree
Hide file tree
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
4 changes: 1 addition & 3 deletions .github/workflows/tests-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ jobs:
name: 'E2E tests'
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
continue-on-error: true
steps:
- uses: actions/checkout@v3
Expand All @@ -241,11 +241,9 @@ jobs:
- name: Run E2E tests
working-directory: packages/e2e
env:
SHOPIFY_FLAG_CLIENT_ID: ${{ secrets.E2E_CLIENT_ID }}
E2E_ACCOUNT_EMAIL: ${{ secrets.E2E_ACCOUNT_EMAIL }}
E2E_ACCOUNT_PASSWORD: ${{ secrets.E2E_ACCOUNT_PASSWORD }}
E2E_STORE_FQDN: ${{ secrets.E2E_STORE_FQDN }}
E2E_SECONDARY_CLIENT_ID: ${{ secrets.E2E_SECONDARY_CLIENT_ID }}
E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }}
run: npx playwright test
- name: Upload Playwright report
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/data/invalid-tomls/bad-syntax.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Invalid TOML: malformed syntax (missing closing quote)
client_id = "__E2E_CLIENT_ID__"
client_id = "1234567890"
name = "Bad Syntax App
application_url = "https://example.com"
embedded = true
2 changes: 1 addition & 1 deletion packages/e2e/data/invalid-tomls/invalid-webhook.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Invalid TOML: bad webhook config (missing required uri)
client_id = "__E2E_CLIENT_ID__"
client_id = "1234567890"
name = "Invalid Webhook App"
application_url = "https://example.com"
embedded = true
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/data/invalid-tomls/wrong-type.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Invalid TOML: wrong types for known fields
client_id = "__E2E_CLIENT_ID__"
client_id = "1234567890"
name = "Wrong Type App"
application_url = "https://example.com"
embedded = "not-a-boolean"
Expand Down
6 changes: 3 additions & 3 deletions packages/e2e/data/valid-app/shopify.app.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Comprehensive shopify.app.toml for E2E regression testing
# client_id is injected at runtime by the toml-app fixture
client_id = "__E2E_CLIENT_ID__"
name = "E2E TOML Regression Test"
# client_id is injected at runtime via injectFixtureToml()
client_id = "__CLIENT_ID__"
name = "__NAME__"
application_url = "https://example.com"
embedded = true

Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineConfig({
maxFailures: isCI ? 3 : 0, // Stop early in CI after 3 failures
reporter: isCI ? [['html', {open: 'never'}], ['list']] : [['list']],
timeout: 3 * 60 * 1000, // 3 minutes per test
globalTimeout: 15 * 60 * 1000, // 15 minutes total
globalTimeout: 30 * 60 * 1000, // 30 minutes total

use: {
trace: isCI ? 'on' : 'off',
Expand Down
58 changes: 36 additions & 22 deletions packages/e2e/setup/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import * as fs from 'fs'
import type {CLIContext, CLIProcess, ExecResult} from './cli.js'
import type {BrowserContext} from './browser.js'

// Env override applied to all CLI helpers — strips CLIENT_ID so commands use the app's own toml.
// NOTE: Do NOT add SHOPIFY_CLI_PARTNERS_TOKEN here. The partners token overrides OAuth in the
// CLI's auth priority, and the App Management API token it exchanges to lacks permissions to
// create apps (403). OAuth provides the full set of required permissions.
const FRESH_APP_ENV = {SHOPIFY_FLAG_CLIENT_ID: undefined}

// ---------------------------------------------------------------------------
// CLI helpers — thin wrappers around cli.exec()
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -45,8 +39,8 @@ export async function createApp(ctx: {
if (ctx.flavor) args.push('--flavor', ctx.flavor)

const result = await cli.execCreateApp(args, {
// Strip CLIENT_ID so the CLI creates a new app instead of linking to a pre-existing one
env: {FORCE_COLOR: '0', ...FRESH_APP_ENV},
// Disable color output and strip CLIENT_ID to prevent leaking from parent process.env
env: {FORCE_COLOR: '0', SHOPIFY_FLAG_CLIENT_ID: undefined},
timeout: 5 * 60 * 1000,
})

Expand Down Expand Up @@ -81,6 +75,33 @@ export async function createApp(ctx: {
return {...result, appDir}
}

// ---------------------------------------------------------------------------
// Fixture helpers — TOML manipulation for test setup
// ---------------------------------------------------------------------------

/**
* Read the client_id from a shopify.app.toml file.
*/
export function extractClientId(appDir: string): string {
const toml = fs.readFileSync(path.join(appDir, 'shopify.app.toml'), 'utf8')
const match = toml.match(/client_id\s*=\s*"([^"]+)"/)
if (!match?.[1]) {
throw new Error(`Could not find client_id in ${path.join(appDir, 'shopify.app.toml')}`)
}
return match[1]
}

/**
* Overwrite a created app's shopify.app.toml with a fixture TOML template.
* The template should contain `__CLIENT_ID__` and `__NAME__` placeholders which get
* replaced with the app's real client_id and the provided name.
*/
export function injectFixtureToml(appDir: string, fixtureTomlContent: string, name: string): void {
const clientId = extractClientId(appDir)
const toml = fixtureTomlContent.replace(/__CLIENT_ID__/g, clientId).replace(/__NAME__/g, name)
fs.writeFileSync(path.join(appDir, 'shopify.app.toml'), toml)
}

export async function generateExtension(
ctx: CLIContext & {
name: string
Expand All @@ -90,11 +111,11 @@ export async function generateExtension(
): Promise<ExecResult> {
const args = ['app', 'generate', 'extension', '--name', ctx.name, '--path', ctx.appDir, '--template', ctx.template]
if (ctx.flavor) args.push('--flavor', ctx.flavor)
return ctx.cli.exec(args, {env: FRESH_APP_ENV, timeout: 5 * 60 * 1000})
return ctx.cli.exec(args, {timeout: 5 * 60 * 1000})
}

export async function buildApp(ctx: CLIContext): Promise<ExecResult> {
return ctx.cli.exec(['app', 'build', '--path', ctx.appDir], {env: FRESH_APP_ENV, timeout: 5 * 60 * 1000})
return ctx.cli.exec(['app', 'build', '--path', ctx.appDir], {timeout: 5 * 60 * 1000})
}

export async function deployApp(
Expand All @@ -112,7 +133,7 @@ export async function deployApp(
if (ctx.version) args.push('--version', ctx.version)
if (ctx.message) args.push('--message', ctx.message)
if (ctx.config) args.push('--config', ctx.config)
return ctx.cli.exec(args, {env: FRESH_APP_ENV, timeout: 5 * 60 * 1000})
return ctx.cli.exec(args, {timeout: 5 * 60 * 1000})
}

export async function appInfo(ctx: CLIContext): Promise<{
Expand All @@ -124,15 +145,15 @@ export async function appInfo(ctx: CLIContext): Promise<{
entrySourceFilePath: string
}[]
}> {
const result = await ctx.cli.exec(['app', 'info', '--path', ctx.appDir, '--json'], {env: FRESH_APP_ENV})
const result = await ctx.cli.exec(['app', 'info', '--path', ctx.appDir, '--json'])
if (result.exitCode !== 0) {
throw new Error(`app info failed (exit ${result.exitCode}):\nstdout: ${result.stdout}\nstderr: ${result.stderr}`)
}
return JSON.parse(result.stdout)
}

export async function functionBuild(ctx: CLIContext): Promise<ExecResult> {
return ctx.cli.exec(['app', 'function', 'build', '--path', ctx.appDir], {env: FRESH_APP_ENV, timeout: 3 * 60 * 1000})
return ctx.cli.exec(['app', 'function', 'build', '--path', ctx.appDir], {timeout: 3 * 60 * 1000})
}

export async function functionRun(
Expand All @@ -141,14 +162,12 @@ export async function functionRun(
},
): Promise<ExecResult> {
return ctx.cli.exec(['app', 'function', 'run', '--path', ctx.appDir, '--input', ctx.inputPath], {
env: FRESH_APP_ENV,
timeout: 60 * 1000,
})
}

export async function versionsList(ctx: CLIContext): Promise<ExecResult> {
return ctx.cli.exec(['app', 'versions', 'list', '--path', ctx.appDir, '--json'], {
env: FRESH_APP_ENV,
timeout: 60 * 1000,
})
}
Expand All @@ -159,7 +178,6 @@ export async function configLink(
},
): Promise<ExecResult> {
return ctx.cli.exec(['app', 'config', 'link', '--path', ctx.appDir, '--client-id', ctx.clientId], {
env: FRESH_APP_ENV,
timeout: 2 * 60 * 1000,
})
}
Expand Down Expand Up @@ -374,13 +392,9 @@ export async function teardownApp(
// ---------------------------------------------------------------------------

export const appTestFixture = authFixture.extend<{authReady: void}>({
// Auto-trigger authLogin and strip CLIENT_ID so tests create their own apps
// Auto-trigger authLogin so the OAuth session is available for all app tests
authReady: [
async (
{authLogin: _authLogin, env}: {authLogin: void; env: import('./env.js').E2EEnv},
use: () => Promise<void>,
) => {
delete env.processEnv.SHOPIFY_FLAG_CLIENT_ID
async ({authLogin: _authLogin}: {authLogin: void}, use: () => Promise<void>) => {
await use()
},
{auto: true},
Expand Down
32 changes: 4 additions & 28 deletions packages/e2e/setup/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,8 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

export interface E2EEnv {
/** Partners token for API auth (empty string if not set) */
partnersToken: string
/** Primary test app client ID (empty string if not set) */
clientId: string
/** Dev store FQDN (e.g. cli-e2e-test.myshopify.com) */
storeFqdn: string
/** Secondary app client ID for config link tests */
secondaryClientId: string
/** Dedicated e2e org ID for fresh-app tests (empty string if not set) */
orgId: string
/** Environment variables to pass to CLI processes */
Expand Down Expand Up @@ -64,19 +58,13 @@ export function createIsolatedEnv(baseDir: string): {tempDir: string; xdgEnv: {[

/**
* Asserts that a required environment variable is set.
* Call this at the top of tests that need auth.
* Call this at the top of tests that need specific env vars.
*/
export function requireEnv(
env: E2EEnv,
...keys: (keyof Pick<E2EEnv, 'partnersToken' | 'clientId' | 'storeFqdn' | 'secondaryClientId' | 'orgId'>)[]
): void {
export function requireEnv(env: E2EEnv, ...keys: (keyof Pick<E2EEnv, 'storeFqdn' | 'orgId'>)[]): void {
for (const key of keys) {
if (!env[key]) {
const envVarNames: {[key: string]: string} = {
partnersToken: 'SHOPIFY_CLI_PARTNERS_TOKEN',
clientId: 'SHOPIFY_FLAG_CLIENT_ID',
storeFqdn: 'E2E_STORE_FQDN',
secondaryClientId: 'E2E_SECONDARY_CLIENT_ID',
orgId: 'E2E_ORG_ID',
}
throw new Error(`${envVarNames[key]} environment variable is required for this test`)
Expand All @@ -85,17 +73,14 @@ export function requireEnv(
}

/**
* Worker-scoped fixture providing auth tokens and environment configuration.
* Auth tokens are optional — tests that need them should call requireEnv().
* Worker-scoped fixture providing environment configuration.
* Env vars are optional — tests that need them should call requireEnv().
*/
export const envFixture = base.extend<{}, {env: E2EEnv}>({
env: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
const partnersToken = process.env.SHOPIFY_CLI_PARTNERS_TOKEN ?? ''
const clientId = process.env.SHOPIFY_FLAG_CLIENT_ID ?? ''
const storeFqdn = process.env.E2E_STORE_FQDN ?? ''
const secondaryClientId = process.env.E2E_SECONDARY_CLIENT_ID ?? ''
const orgId = process.env.E2E_ORG_ID ?? ''

const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(directories.root, '.e2e-tmp')
Expand All @@ -112,21 +97,12 @@ export const envFixture = base.extend<{}, {env: E2EEnv}>({
SHOPIFY_CLI_1P_DEV: undefined,
}

if (partnersToken) {
processEnv.SHOPIFY_CLI_PARTNERS_TOKEN = partnersToken
}
if (clientId) {
processEnv.SHOPIFY_FLAG_CLIENT_ID = clientId
}
if (storeFqdn) {
processEnv.SHOPIFY_FLAG_STORE = storeFqdn
}

const env: E2EEnv = {
partnersToken,
clientId,
storeFqdn,
secondaryClientId,
orgId,
processEnv,
tempDir,
Expand Down
34 changes: 0 additions & 34 deletions packages/e2e/setup/toml-app.ts

This file was deleted.

Loading
Loading