Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,164 @@
import {createToolsTypeDefinition} from './type-generation.js'
import {
createIntentsTypeDefinition,
createToolsTypeDefinition,
getGeneratedTypesHelperImportPath,
} from './type-generation.js'
import {AbortError} from '@shopify/cli-kit/node/error'
import {describe, expect, test} from 'vitest'

const adminGeneratedTypesHelperImportPath = '@shopify/ui-extensions/admin'

describe('getGeneratedTypesHelperImportPath', () => {
test('returns the surface package for generated helper types', () => {
expect(getGeneratedTypesHelperImportPath(['admin.app.intent.render'])).toBe('@shopify/ui-extensions/admin')
expect(getGeneratedTypesHelperImportPath(['purchase.checkout.block.render'])).toBe(
'@shopify/ui-extensions/checkout',
)
expect(getGeneratedTypesHelperImportPath(['pos.home.tile.render'])).toBe('@shopify/ui-extensions/point-of-sale')
expect(getGeneratedTypesHelperImportPath(['customer-account.order-status.block.render'])).toBe(
'@shopify/ui-extensions/customer-account',
)
})
})

describe('createIntentsTypeDefinition', () => {
test('returns empty string when intents array is empty', async () => {
// When
const result = await createIntentsTypeDefinition([], {
generatedTypesHelperImportPath: adminGeneratedTypesHelperImportPath,
})

// Then
expect(result).toBe('')
})

test('generates request and response types for a single intent schema', async () => {
// Given
const intents = [
{
action: 'create',
type: 'application/email',
inputSchema: {
type: 'object',
properties: {
recipient: {type: 'string'},
},
required: ['recipient'],
},
outputSchema: {
type: 'object',
properties: {
success: {type: 'boolean'},
},
},
},
]

// When
const result = await createIntentsTypeDefinition(intents, {
generatedTypesHelperImportPath: adminGeneratedTypesHelperImportPath,
})

// Then
expect(result).toBe(`interface CreateApplicationEmailIntentInput {
recipient: string;
[k: string]: unknown;
}

type CreateApplicationEmailIntentValue = unknown
interface CreateApplicationEmailIntentOutput {
success?: boolean;
[k: string]: unknown;
}

interface CreateApplicationEmailIntentRequest {
action: "create";
type: "application/email";
data: CreateApplicationEmailIntentInput;
value?: CreateApplicationEmailIntentValue;
}

type ShopifyGeneratedIntentVariants =
| import('@shopify/ui-extensions/admin').ShopifyGeneratedIntentVariant<CreateApplicationEmailIntentRequest, CreateApplicationEmailIntentOutput>
`)
})

test('supports multiple intents with value schemas', async () => {
// Given
const intents = [
{
action: 'create',
type: 'application/email',
inputSchema: {
type: 'object',
properties: {
recipient: {type: 'string'},
},
},
},
{
action: 'edit',
type: 'shopify/Product',
valueSchema: {
type: 'string',
},
inputSchema: {
type: 'object',
properties: {
title: {type: 'string'},
},
},
outputSchema: {
type: 'object',
properties: {
id: {type: 'string'},
},
},
},
]

// When
const result = await createIntentsTypeDefinition(intents, {
generatedTypesHelperImportPath: adminGeneratedTypesHelperImportPath,
})

// Then
expect(result).toContain('interface CreateApplicationEmailIntentRequest')
expect(result).toContain('type CreateApplicationEmailIntentOutput = unknown')
expect(result).toContain('interface EditShopifyProductIntentRequest')
expect(result).toContain('type EditShopifyProductIntentValue = string')
expect(result).not.toContain('ShopifyGeneratedIntentsApi')
expect(result).toContain(
"import('@shopify/ui-extensions/admin').ShopifyGeneratedIntentVariant<EditShopifyProductIntentRequest, EditShopifyProductIntentOutput>",
)
})

test('throws AbortError when intent action/type pairs are duplicated', async () => {
// Given
const intents = [
{
action: 'create',
type: 'application/email',
inputSchema: {type: 'object'},
},
{
action: 'create',
type: 'application/email',
inputSchema: {type: 'object'},
},
]

// When & Then
await expect(
createIntentsTypeDefinition(intents, {generatedTypesHelperImportPath: adminGeneratedTypesHelperImportPath}),
).rejects.toThrow(
new AbortError(
'Intent "create:application/email" is defined multiple times. Intents must be unique within a target.',
),
)
})
})

describe('createToolsTypeDefinition', () => {
test('returns empty string when tools array is empty', async () => {
// When
Expand Down Expand Up @@ -53,7 +210,7 @@ interface ShopifyTools {
/**
* Gets a product by ID
*/
register(name: 'get_product', handler: (input: GetProductInput) => GetProductOutput | Promise<GetProductOutput>);
register(name: 'get_product', handler: (input: GetProductInput) => GetProductOutput | Promise<GetProductOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -87,7 +244,7 @@ interface ShopifyTools {
/**
* A simple action
*/
register(name: 'simple_action', handler: (input: SimpleActionInput) => SimpleActionOutput | Promise<SimpleActionOutput>);
register(name: 'simple_action', handler: (input: SimpleActionInput) => SimpleActionOutput | Promise<SimpleActionOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -157,11 +314,11 @@ interface ShopifyTools {
/**
* First tool
*/
register(name: 'tool_one', handler: (input: ToolOneInput) => ToolOneOutput | Promise<ToolOneOutput>);
register(name: 'tool_one', handler: (input: ToolOneInput) => ToolOneOutput | Promise<ToolOneOutput>): () => void;
/**
* Second tool
*/
register(name: 'tool_two', handler: (input: ToolTwoInput) => ToolTwoOutput | Promise<ToolTwoOutput>);
register(name: 'tool_two', handler: (input: ToolTwoInput) => ToolTwoOutput | Promise<ToolTwoOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -212,7 +369,7 @@ interface ShopifyTools {
/**
* This description contains *\\/ which could break comments
*/
register(name: 'tool_with_special_desc', handler: (input: ToolWithSpecialDescInput) => ToolWithSpecialDescOutput | Promise<ToolWithSpecialDescOutput>);
register(name: 'tool_with_special_desc', handler: (input: ToolWithSpecialDescInput) => ToolWithSpecialDescOutput | Promise<ToolWithSpecialDescOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -242,7 +399,7 @@ interface ShopifyTools {
* Line two
* Line three
*/
register(name: 'documented_tool', handler: (input: DocumentedToolInput) => DocumentedToolOutput | Promise<DocumentedToolOutput>);
register(name: 'documented_tool', handler: (input: DocumentedToolInput) => DocumentedToolOutput | Promise<DocumentedToolOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -274,7 +431,7 @@ interface ShopifyTools {
/**
* A tool with snake case name
*/
register(name: 'my_snake_case_tool', handler: (input: MySnakeCaseToolInput) => MySnakeCaseToolOutput | Promise<MySnakeCaseToolOutput>);
register(name: 'my_snake_case_tool', handler: (input: MySnakeCaseToolInput) => MySnakeCaseToolOutput | Promise<MySnakeCaseToolOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -345,7 +502,7 @@ interface ShopifyTools {
/**
* A tool with nested schema
*/
register(name: 'complex_tool', handler: (input: ComplexToolInput) => ComplexToolOutput | Promise<ComplexToolOutput>);
register(name: 'complex_tool', handler: (input: ComplexToolInput) => ComplexToolOutput | Promise<ComplexToolOutput>): () => void;
}
`)
})
Expand Down Expand Up @@ -373,11 +530,44 @@ interface ShopifyTools {
/**
* Gets product info
*/
register(name: 'get-product-info', handler: (input: GetProductInfoInput) => GetProductInfoOutput | Promise<GetProductInfoOutput>);
register(name: 'get-product-info', handler: (input: GetProductInfoInput) => GetProductInfoOutput | Promise<GetProductInfoOutput>): () => void;
}
`)
})

test('renames types generated from schemas with titles to the requested generated name', async () => {
// Given
const tools = [
{
name: 'expected_tool',
description: 'A tool with schema titles',
inputSchema: {
title: 'SchemaTitleInput',
type: 'object',
properties: {
id: {type: 'string'},
},
},
outputSchema: {
title: 'SchemaTitleOutput',
type: 'object',
properties: {
success: {type: 'boolean'},
},
},
},
]

// When
const result = await createToolsTypeDefinition(tools)

// Then
expect(result).toContain('interface ExpectedToolInput')
expect(result).toContain('interface ExpectedToolOutput')
expect(result).not.toContain('interface SchemaTitleInput')
expect(result).not.toContain('interface SchemaTitleOutput')
})

test('does not include export declarations in the output', async () => {
// Given
// We need to ensure export declarations are stripped from the output since these types
Expand Down
Loading
Loading