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
7 changes: 5 additions & 2 deletions packages/toolbars/upload/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"access": "public"
},
"scripts": {
"build": "vite build"
"build": "vite build",
"test": "vitest",
"test:unit": "vitest run"
},
"type": "module",
"main": "dist/index.js",
Expand Down Expand Up @@ -35,7 +37,8 @@
"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"vite": "^5.4.2"
"vite": "^5.4.2",
"vitest": "^1.4.0"
},
"peerDependencies": {
"@opentiny/vue": "^3.20.0",
Expand Down
96 changes: 96 additions & 0 deletions packages/toolbars/upload/test/assetImport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, expect, it } from 'vitest'
import {
buildImportedAssetCreatePayload,
buildImportedAssetResourceName,
findImportedAssetResource,
getImportedAssetResourceUrl,
getImportedAssets,
normalizeImportedAssetResourceList,
replaceImportedAssetPlaceholders,
splitImportedAssetCreatePayloads
} from '../src/assetImport'

describe('asset import helpers', () => {
it('should keep only importable assets from an app schema', () => {
const valid = { placeholder: '__ASSET_1__', resourceData: 'data:image/png;base64,abc' }
const assets = getImportedAssets({
assets: [valid, { placeholder: '__ASSET_2__' }, { resourceData: 'missing' }, null]
})

expect(assets).toEqual([valid])
expect(getImportedAssets({ assets: [] })).toEqual([])
expect(getImportedAssets(undefined)).toEqual([])
})

it('should generate deterministic sanitized resource names and create payloads', () => {
const asset = { filePath: 'src/assets/app logo.JPG', resourceData: 'binary-data' }
const name = buildImportedAssetResourceName(asset)

expect(name).toMatch(/^app_logo_[a-z0-9]+\.jpg$/)
expect(buildImportedAssetResourceName(asset)).toBe(name)
expect(buildImportedAssetCreatePayload(asset, 42, { appId: 'app-1', platformId: 'platform-1' })).toEqual({
name,
description: asset.filePath,
resourceGroupId: 42,
resourceData: asset.resourceData,
resourceUrl: '',
category: 'image',
appId: 'app-1',
platformId: 'platform-1'
})
})

it('should split upload payloads by batch count and serialized size', () => {
const payloads = [{ id: 1 }, { id: 2 }, { id: 3 }]

expect(splitImportedAssetCreatePayloads(payloads, { maxBatchSize: 2 })).toEqual([
[{ id: 1 }, { id: 2 }],
[{ id: 3 }]
])
expect(splitImportedAssetCreatePayloads(payloads, { maxPayloadSize: 1 })).toEqual([
[{ id: 1 }],
[{ id: 2 }],
[{ id: 3 }]
])
expect(splitImportedAssetCreatePayloads([])).toEqual([])
})

it('should replace asset placeholders recursively without changing non-string values', () => {
const schema = {
image: '__ASSET_1__',
nested: [{ css: 'url(__ASSET_1__)' }, { value: 1 }],
expression: { type: 'JSExpression', value: "'__ASSET_1__'" }
}
const replacements = new Map([['__ASSET_1__', 'https://cdn.test/logo.png']])

expect(replaceImportedAssetPlaceholders(schema, replacements)).toEqual({
image: 'https://cdn.test/logo.png',
nested: [{ css: 'url(https://cdn.test/logo.png)' }, { value: 1 }],
expression: { type: 'JSExpression', value: "'https://cdn.test/logo.png'" }
})
expect(replaceImportedAssetPlaceholders(schema, new Map())).toBe(schema)
})

it('should resolve resource URLs from supported response field names', () => {
expect(getImportedAssetResourceUrl({ resourceUrl: '/resource.png', url: '/fallback.png' })).toBe('/resource.png')
expect(getImportedAssetResourceUrl({ download_url: '/download.png' })).toBe('/download.png')
expect(getImportedAssetResourceUrl({})).toBe('')
expect(findImportedAssetResource([{ name: 'logo_hash.png' }], { name: 'logo_hash.png' })).toEqual({
name: 'logo_hash.png'
})
expect(findImportedAssetResource([{ description: 'src/logo.png' }], { description: 'src/logo.png' })).toEqual({
description: 'src/logo.png'
})
expect(findImportedAssetResource([], { name: 'missing' })).toBeNull()
})

it('should normalize resource list responses from common API envelopes', () => {
const list = [{ id: 1 }]

expect(normalizeImportedAssetResourceList(list)).toBe(list)
expect(normalizeImportedAssetResourceList({ data: list })).toBe(list)
expect(normalizeImportedAssetResourceList({ data: { records: list } })).toBe(list)
expect(normalizeImportedAssetResourceList({ list })).toBe(list)
expect(normalizeImportedAssetResourceList({ data: {} })).toEqual([])
})
})
75 changes: 75 additions & 0 deletions packages/toolbars/upload/test/blockImport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest'
import {
buildImportedBlockEvents,
inferImportedBlockPropTypeFromValue,
normalizeImportedBlockDefaultValue,
normalizeImportedBlockPropName,
normalizeImportedBlockPropType,
resolveImportedBlockDefaultValue,
resolveImportedBlockPropType,
splitImportedBlockBindings,
toImportedBlockEventKey
} from '../src/blockImport'

describe('block import helpers', () => {
it('should normalize kebab-case block prop names and event keys', () => {
expect(normalizeImportedBlockPropName(' model-value ')).toBe('modelValue')
expect(normalizeImportedBlockPropName('')).toBe('')
expect(toImportedBlockEventKey('save')).toBe('onSave')
expect(toImportedBlockEventKey('update:modelValue')).toBe('onUpdate:modelValue')
expect(toImportedBlockEventKey('onCancel')).toBe('onCancel')
})

it('should split block props and event bindings while ignoring structural keys', () => {
const result = splitImportedBlockBindings({
'model-value': 1,
disabled: true,
onSave: { type: 'JSExpression', value: 'save' },
'onUpdate:modelValue': 'update',
key: 'row-1',
ref: 'button'
})

expect(result).toEqual({
props: { modelValue: 1, disabled: true },
events: { onSave: { type: 'JSExpression', value: 'save' }, 'onUpdate:modelValue': 'update' }
})
})

it('should build deduplicated event metadata from emits and bindings', () => {
const events = buildImportedBlockEvents(['save', 'update:modelValue', 'save'], { onCancel: true, ignored: true })

expect(Object.keys(events).sort()).toEqual(['onCancel', 'onSave', 'onUpdate:modelValue'].sort())
expect(events.onSave).toEqual({
name: 'onSave',
label: { zh_CN: 'onSave' },
description: { zh_CN: 'onSave' }
})
})

it('should normalize declared and inferred block property types', () => {
expect(normalizeImportedBlockPropType('Array<string>')).toBe('array')
expect(normalizeImportedBlockPropType('string | null | undefined')).toBe('string')
expect(normalizeImportedBlockPropType('number | 1')).toBe('number')
expect(normalizeImportedBlockPropType('() => void')).toBe('function')
expect(normalizeImportedBlockPropType('Record<string, unknown>')).toBe('object')
expect(normalizeImportedBlockPropType('unknown')).toBe('string')
expect(inferImportedBlockPropTypeFromValue([1])).toBe('array')
expect(inferImportedBlockPropTypeFromValue({})).toBe('object')
expect(inferImportedBlockPropTypeFromValue({ type: 'JSFunction', value: 'function() {}' })).toBe('function')
expect(resolveImportedBlockPropType('', false)).toBe('boolean')
expect(resolveImportedBlockPropType('Array<string>', [])).toBe('array')
})

it('should resolve literal, dynamic and fallback default values by type', () => {
const dynamicNumber = { type: 'JSExpression', value: 'this.state.count' }

expect(normalizeImportedBlockDefaultValue(null)).toBe('')
expect(normalizeImportedBlockDefaultValue(dynamicNumber)).toEqual(dynamicNumber)
expect(resolveImportedBlockDefaultValue(undefined, 'ready', 'string')).toBe('ready')
expect(resolveImportedBlockDefaultValue(undefined, dynamicNumber, 'number')).toBe(0)
expect(resolveImportedBlockDefaultValue(3, dynamicNumber, 'number')).toBe(3)
expect(resolveImportedBlockDefaultValue(undefined, undefined, 'boolean')).toBe(false)
expect(resolveImportedBlockDefaultValue(undefined, undefined, 'array')).toEqual([])
})
})
119 changes: 119 additions & 0 deletions packages/toolbars/upload/test/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getMetaApi, getMergeMeta, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
import {
batchCreateResource,
createBlock,
createBlockGroup,
createDataSource,
createResourceGroup,
createUtilsResource,
deployBlock,
fetchBlockByLabel,
fetchBlockGroups,
fetchDataSourceList,
fetchPageList,
fetchResourceGroups,
fetchResourceListByGroupId,
fetchUtilsResourceList,
updateAppConfig,
updateBlock,
updateDataSource,
updateUtilsResource
} from '../src/http'

vi.mock('@opentiny/tiny-engine-meta-register', () => ({
getMetaApi: vi.fn(),
getMergeMeta: vi.fn(),
callEntry: vi.fn((entry: any) => entry),
META_SERVICE: { GlobalService: 'GlobalService', Http: 'Http' }
}))

describe('upload HTTP helpers', () => {
const http = { get: vi.fn(), post: vi.fn() }

beforeEach(() => {
vi.clearAllMocks()
vi.mocked(getMetaApi).mockReturnValue(http as any)
vi.mocked(getMergeMeta).mockReturnValue({ platformId: 'platform-1' } as any)
vi.mocked(getMetaApi).mockImplementation((service: any) => {
if (service === META_SERVICE.GlobalService) {
return { getBaseInfo: () => ({ id: 'app-1' }) } as any
}
return http as any
})
})

it('should request page and block lists with the expected query parameters', () => {
fetchPageList('app-1')
expect(http.get).toHaveBeenCalledWith('/app-center/api/pages/list/app-1')

fetchBlockGroups({ page: 2, size: 10 })
expect(http.get).toHaveBeenCalledWith('/material-center/api/block-groups', {
params: { page: 2, size: 10, from: 'block' }
})

fetchBlockByLabel('hero block')
expect(http.get).toHaveBeenCalledWith('/material-center/api/block?label=hero block')
})
Comment thread
xuanlid marked this conversation as resolved.

it('should create, update and deploy blocks through the matching endpoints', () => {
const group = { name: 'Imported' }
const block = { label: 'Hero', content: {} }
const update = { content: { componentName: 'Block' } }

createBlockGroup(group)
createBlock(block)
updateBlock('block-1', update, 'app-1')
deployBlock({ id: 'block-1' })

expect(http.post).toHaveBeenNthCalledWith(1, '/material-center/api/block-groups/create', group)
expect(http.post).toHaveBeenNthCalledWith(2, '/material-center/api/block/create', block)
expect(http.post).toHaveBeenNthCalledWith(3, '/material-center/api/block/update/block-1', update, {
params: { appId: 'app-1' }
})
expect(http.post).toHaveBeenNthCalledWith(4, '/material-center/api/block/deploy', { id: 'block-1' })
})

it('should expose utils and data source endpoints with app identifiers', () => {
fetchUtilsResourceList('app-1')
createUtilsResource({ name: 'format' })
updateUtilsResource({ id: 'util-1', name: 'format' })
fetchDataSourceList(7)
createDataSource({ name: 'users' })
updateDataSource(9, { name: 'users' })
updateAppConfig('app-1', { description: 'Imported' })

expect(http.get).toHaveBeenCalledWith('/app-center/api/apps/extension/list?app=app-1&category=utils')
expect(http.get).toHaveBeenCalledWith('/app-center/api/sources/list/7')
expect(http.post).toHaveBeenCalledWith('/app-center/api/apps/extension/create', { name: 'format' })
expect(http.post).toHaveBeenCalledWith('/app-center/api/apps/extension/update', { id: 'util-1', name: 'format' })
expect(http.post).toHaveBeenCalledWith('/app-center/api/sources/create', { name: 'users' })
expect(http.post).toHaveBeenCalledWith('/app-center/api/sources/update/9', { name: 'users' })
expect(http.post).toHaveBeenCalledWith('/app-center/api/apps/update/app-1', { description: 'Imported' })
})

it('should include app and platform metadata for resource group and batch uploads', () => {
const group = { name: 'Imported assets', description: 'Images' }
const resources = [{ name: 'logo.png', resourceData: 'data:image/png;base64,abc' }]

fetchResourceGroups()
createResourceGroup(group)
fetchResourceListByGroupId('group-1')
batchCreateResource(resources)

expect(http.get).toHaveBeenCalledWith('/material-center/api/resource-group/app-1')
expect(http.get).toHaveBeenCalledWith('/material-center/api/resource/find/group-1')
expect(http.post).toHaveBeenCalledWith('/material-center/api/resource-group/create', {
...group,
appId: 'app-1',
platformId: 'platform-1'
})
expect(http.post).toHaveBeenCalledWith('/material-center/api/resource/create/batch', [
{
...resources[0],
appId: 'app-1',
platformId: 'platform-1'
}
])
})
})
Loading
Loading