diff --git a/packages/e2e/setup/app.ts b/packages/e2e/setup/app.ts index 7073b38a395..1053c96d8e8 100644 --- a/packages/e2e/setup/app.ts +++ b/packages/e2e/setup/app.ts @@ -2,7 +2,6 @@ import {authFixture} from './auth.js' import {getLastPageStatus, isVisibleWithin, navigateToDashboard, refreshIfPageError} from './browser.js' import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' -import {updateTomlValues} from '@shopify/toml-patch' import * as toml from '@iarna/toml' import * as path from 'path' import * as fs from 'fs' @@ -112,17 +111,41 @@ export function extractClientId(appDir: string): string { } /** - * Overwrite a created app's shopify.app.toml with a fixture TOML template. - * Uses toml-patch to surgically set client_id and name while - * preserving comments and formatting in the fixture file. + * Merge an E2E TOML fixture into the app config generated by the template. + * Template-owned fields not mentioned by the fixture are preserved. */ +type TomlTable = Parameters[0] + +function isTomlTable(value: unknown): value is TomlTable { + return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date) +} + +function mergeTomlTables(base: TomlTable, overlay: TomlTable): TomlTable { + const merged: TomlTable = {...base} + for (const [key, overlayValue] of Object.entries(overlay)) { + const baseValue = merged[key] + merged[key] = + isTomlTable(baseValue) && isTomlTable(overlayValue) ? mergeTomlTables(baseValue, overlayValue) : overlayValue + } + return merged +} + +export function mergeFixtureToml(generatedTomlContent: string, fixtureTomlContent: string, name: string): string { + const generated = toml.parse(generatedTomlContent) + const clientId = generated.client_id as string | undefined + if (!clientId) { + throw new Error('Could not find client_id in generated shopify.app.toml') + } + + const fixture = toml.parse(fixtureTomlContent) + + return toml.stringify(mergeTomlTables(generated, {...fixture, client_id: clientId, name})) +} + export function injectFixtureToml(appDir: string, fixtureTomlContent: string, name: string): void { - const clientId = extractClientId(appDir) - const patched = updateTomlValues(fixtureTomlContent, [ - [['client_id'], clientId], - [['name'], name], - ]) - fs.writeFileSync(path.join(appDir, 'shopify.app.toml'), patched) + const tomlPath = path.join(appDir, 'shopify.app.toml') + const patched = mergeFixtureToml(fs.readFileSync(tomlPath, 'utf8'), fixtureTomlContent, name) + fs.writeFileSync(tomlPath, patched) } export async function generateExtension( diff --git a/packages/e2e/tests/fixture-toml.spec.ts b/packages/e2e/tests/fixture-toml.spec.ts new file mode 100644 index 00000000000..e28ba61b40d --- /dev/null +++ b/packages/e2e/tests/fixture-toml.spec.ts @@ -0,0 +1,79 @@ +/* eslint-disable no-restricted-imports */ +import {mergeFixtureToml} from '../setup/app.js' +import {expect, test} from '@playwright/test' +import * as toml from '@iarna/toml' +import * as fs from 'fs' +import * as path from 'path' +import {fileURLToPath} from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const VALID_APP_FIXTURE = fs.readFileSync(path.join(__dirname, '../data/valid-app/shopify.app.toml'), 'utf8') + +test.describe('fixture TOML', () => { + test('merges fixture values without dropping template-owned fields', () => { + const generatedToml = ` +client_id = "generated-client-id" +name = "Generated app name" +application_url = "https://template.example.com" + +[access_scopes] +scopes = "read_products" + +[sidekick] +extensions_summary = "Template-provided Sidekick summary" +template_owned = true +`.trimStart() + + const fixtureToml = ` +client_id = "placeholder" +name = "placeholder" +application_url = "https://fixture.example.com" + +[build] +include_config_on_deploy = true + +[sidekick] +extensions_summary = "Fixture-provided Sidekick summary" +`.trimStart() + + const mergedToml = mergeFixtureToml(generatedToml, fixtureToml, 'E2E merged app') + const parsed = toml.parse(mergedToml) + + expect(parsed.client_id).toBe('generated-client-id') + expect(parsed.name).toBe('E2E merged app') + expect(parsed.application_url).toBe('https://fixture.example.com') + expect(parsed.access_scopes).toEqual({scopes: 'read_products'}) + expect(parsed.sidekick).toEqual({ + extensions_summary: 'Fixture-provided Sidekick summary', + template_owned: true, + }) + expect(parsed.build).toEqual({include_config_on_deploy: true}) + }) + + test('valid app fixture can merge into generated template TOML', () => { + const generatedToml = ` +client_id = "generated-client-id" +name = "Generated app name" + +[sidekick] +extensions_summary = "Template-provided Sidekick summary" +template_owned = true + +[template_owned] +kept = true +`.trimStart() + + const mergedToml = mergeFixtureToml(generatedToml, VALID_APP_FIXTURE, 'E2E valid fixture app') + const parsed = toml.parse(mergedToml) + + expect(parsed.client_id).toBe('generated-client-id') + expect(parsed.name).toBe('E2E valid fixture app') + expect(parsed.template_owned).toEqual({kept: true}) + expect(parsed.sidekick).toEqual({ + extensions_summary: 'Read, create, and edit FAQ entries stored in the app', + template_owned: true, + }) + expect(parsed.webhooks).toMatchObject({api_version: '2025-01'}) + expect((parsed.webhooks as {subscriptions: unknown[]}).subscriptions).toHaveLength(2) + }) +})