|
| 1 | +import Publish from './publish.js' |
| 2 | +import {themePublishJsonOutputSchema} from '../../services/publish/types.js' |
| 3 | +import {findOrSelectTheme} from '../../utilities/theme-selector.js' |
| 4 | +import {Config} from '@oclif/core' |
| 5 | +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' |
| 6 | +import {themePublish} from '@shopify/cli-kit/node/themes/api' |
| 7 | +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' |
| 8 | +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' |
| 9 | +import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' |
| 10 | +import {joinPath} from '@shopify/cli-kit/node/path' |
| 11 | +import {outputWarn} from '@shopify/cli-kit/node/output' |
| 12 | +import {describe, expect, test, vi} from 'vitest' |
| 13 | + |
| 14 | +vi.mock('@shopify/cli-kit/node/session') |
| 15 | +vi.mock('@shopify/cli-kit/node/themes/api') |
| 16 | +vi.mock('@shopify/cli-kit/node/metadata') |
| 17 | +vi.mock('../../utilities/theme-selector.js') |
| 18 | +vi.mock('../../utilities/theme-store.js', () => ({ensureThemeStore: ({store}: {store: string}) => store})) |
| 19 | + |
| 20 | +const originalTheme = {id: 1, name: 'Original', role: 'unpublished', processing: false, createdAtRuntime: false} |
| 21 | +const publishedTheme = {...originalTheme, role: 'live'} |
| 22 | +const store = 'test.myshopify.com' |
| 23 | + |
| 24 | +async function run(argv: string[]) { |
| 25 | + const config = new Config({root: __dirname}) |
| 26 | + await config.load() |
| 27 | + vi.mocked(ensureAuthenticatedThemes).mockImplementation(async (storeFqdn) => ({token: 'token', storeFqdn})) |
| 28 | + return runWithCommandEventsForCommand(argv, () => new Publish(argv, config).run()) |
| 29 | +} |
| 30 | + |
| 31 | +async function inEnvironments(run: () => Promise<void>) { |
| 32 | + await inTemporaryDirectory(async (directory) => { |
| 33 | + await writeFile( |
| 34 | + joinPath(directory, 'shopify.theme.toml'), |
| 35 | + ` |
| 36 | +[environments.first] |
| 37 | +store = "first.myshopify.com" |
| 38 | +password = "token" |
| 39 | +[environments.second] |
| 40 | +store = "second.myshopify.com" |
| 41 | +password = "token" |
| 42 | +`, |
| 43 | + ) |
| 44 | + const cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory) |
| 45 | + try { |
| 46 | + await run() |
| 47 | + } finally { |
| 48 | + cwd.mockRestore() |
| 49 | + } |
| 50 | + }) |
| 51 | +} |
| 52 | + |
| 53 | +describe('theme publish JSON output', () => { |
| 54 | + test('exposes its schema and JSON flag in help', () => { |
| 55 | + expect(Publish.jsonOutputSchema).toBe(themePublishJsonOutputSchema) |
| 56 | + expect(Publish.flags.json).toBeDefined() |
| 57 | + expect(Publish.description).toContain('ThemePublishResult') |
| 58 | + }) |
| 59 | + |
| 60 | + test.each([undefined, '', 'https://example.com/theme.zip'])( |
| 61 | + 'returns the updated theme and omits missing src (%s)', |
| 62 | + async (src) => { |
| 63 | + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) |
| 64 | + vi.mocked(themePublish).mockResolvedValue({...publishedTheme, src}) |
| 65 | + await withCapturedStandardStreams(async ({stdout, stderr}) => { |
| 66 | + await run(['--store', store, '--theme', '1', '--force', '--json']) |
| 67 | + expect(JSON.parse(stdout())).toEqual({ |
| 68 | + theme: {...publishedTheme, ...(src === undefined ? {} : {src}), shop: store}, |
| 69 | + }) |
| 70 | + expect(stderr()).toBe('') |
| 71 | + }) |
| 72 | + }, |
| 73 | + ) |
| 74 | + |
| 75 | + test('routes diagnostics to stderr', async () => { |
| 76 | + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) |
| 77 | + vi.mocked(themePublish).mockImplementation(async () => { |
| 78 | + outputWarn('Retrying request') |
| 79 | + return publishedTheme |
| 80 | + }) |
| 81 | + await withCapturedStandardStreams(async ({stdout, stderr}) => { |
| 82 | + await run(['--store', store, '--theme', '1', '--force', '--json']) |
| 83 | + expect(JSON.parse(stdout()).theme.role).toBe('live') |
| 84 | + expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'Retrying request'}) |
| 85 | + }) |
| 86 | + }) |
| 87 | + |
| 88 | + test('leaves execution failures to the shared error handler without writing a result', async () => { |
| 89 | + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) |
| 90 | + vi.mocked(themePublish).mockRejectedValue(new Error('Publishing failed')) |
| 91 | + await withCapturedStandardStreams(async ({stdout}) => { |
| 92 | + await expect(run(['--store', store, '--theme', '1', '--force', '--json'])).rejects.toThrow('Publishing failed') |
| 93 | + expect(stdout()).toBe('') |
| 94 | + }) |
| 95 | + }) |
| 96 | + |
| 97 | + test.each(['none', 'partial', 'all'])('collects one document in configured order (%s failures)', async (failures) => { |
| 98 | + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) |
| 99 | + vi.mocked(themePublish).mockImplementation(async (_id, session) => { |
| 100 | + if (failures === 'all' || (failures === 'partial' && session.storeFqdn.startsWith('first.'))) { |
| 101 | + throw new Error('Publishing failed') |
| 102 | + } |
| 103 | + return publishedTheme |
| 104 | + }) |
| 105 | + const exitCode = process.exitCode |
| 106 | + await inEnvironments(async () => { |
| 107 | + await withCapturedStandardStreams(async ({stdout, stderr}) => { |
| 108 | + await run(['--environment', 'first', '--environment', 'second', '--theme', '1', '--force', '--json']) |
| 109 | + const environments: string[] = [] |
| 110 | + if (failures === 'none') environments.push('first') |
| 111 | + if (failures !== 'all') environments.push('second') |
| 112 | + expect(JSON.parse(stdout())).toEqual({ |
| 113 | + environments: environments.map((environment) => ({ |
| 114 | + environment, |
| 115 | + result: {theme: {...publishedTheme, shop: `${environment}.myshopify.com`}}, |
| 116 | + })), |
| 117 | + }) |
| 118 | + if (failures === 'none') { |
| 119 | + expect(stderr()).toBe('') |
| 120 | + } else { |
| 121 | + const errors = stderr() |
| 122 | + .trim() |
| 123 | + .split('\n') |
| 124 | + .map((line) => JSON.parse(line)) |
| 125 | + expect(errors).toHaveLength(failures === 'all' ? 2 : 1) |
| 126 | + expect(errors[0]).toMatchObject({type: 'diagnostic', level: 'error', code: 'theme-environment-failed'}) |
| 127 | + } |
| 128 | + expect(process.exitCode).toBe(exitCode) |
| 129 | + }) |
| 130 | + }) |
| 131 | + }) |
| 132 | + |
| 133 | + test.each([ |
| 134 | + {theme: {...publishedTheme, id: '1', shop: store}}, |
| 135 | + {theme: {...publishedTheme, shop: null}}, |
| 136 | + {theme: {...publishedTheme, src: false, shop: store}}, |
| 137 | + {environments: [{environment: 'first', result: {theme: {...publishedTheme, role: null, shop: store}}}]}, |
| 138 | + ])('rejects malformed results %#', (result) => { |
| 139 | + expect(() => themePublishJsonOutputSchema.validate(result)).toThrow() |
| 140 | + }) |
| 141 | +}) |
0 commit comments