import { beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest' import { LibraryModel, StylesheetModel, Transport } from '../src/index.js' import * as Style from '../src/style/index.js' import { Stylesheet } from '../types/index.js' import { default as fetch, Response } from 'node-fetch' import { Platform, VariantStyle } from '../types/style/types/basic/font.js' const testStyle: Omit = { id: 'default_screen', source: [ { type: 'font', props: { platform: 'google', variants: [ { name: 'regular', style: 'normal', weight: 400, familyName: 'Nunito Sans' }, { name: '700', style: 'normal', weight: 700, familyName: 'Nunito Sans' }, { name: '800', style: 'normal', weight: 800, familyName: 'Nunito Sans' }, { name: '900', style: 'normal', weight: 900, familyName: 'Nunito Sans' } ], familyName: 'Nunito Sans' }, details: { id: 'RVADHP5mrh', slug: '', title: '', description: ' ', exampleContent: null }, stylesheetId: 'default_screen' }, { type: 'font', props: { platform: 'google', variants: [ { name: '700', style: 'normal', weight: 700, familyName: 'Fraunces' }, { name: '800', style: 'normal', weight: 800, familyName: 'Fraunces' }, { name: '700italic', style: 'italic', weight: 700, familyName: 'Fraunces' }, { name: '800italic', style: 'italic', weight: 800, familyName: 'Fraunces' } ], familyName: 'Fraunces' }, details: { id: 'fFELCHdnc8', slug: '', title: '', description: 'Decorative', exampleContent: null }, stylesheetId: 'default_screen' }, { type: 'decoration', props: { borderColor: { id: 's_VbwveFTp', alpha: 1 }, borderStyle: 'solid', borderTopWidth: { unit: 'px', value: 3 }, boxShadowColor: { id: 's_VbwveFTp', alpha: '0.70' }, boxShadowInset: false, borderLeftWidth: { unit: 'px', value: 3 }, borderRightWidth: { unit: 'px', value: 3 }, boxShadowOffsetX: { unit: 'px', value: 4 }, boxShadowOffsetY: { unit: 'px', value: 4 }, borderBottomWidth: { unit: 'px', value: 3 }, borderTopLeftRadius: { unit: 'px', value: 0 }, boxShadowBlurRadius: { unit: 'px', value: 0 }, borderTopRightRadius: { unit: 'px', value: 0 }, boxShadowSpreadRadius: { unit: 'px', value: 0 }, borderBottomLeftRadius: { unit: 'px', value: 0 }, borderBottomRightRadius: { unit: 'px', value: 0 } }, details: { id: '35oPoKoFjW', slug: 'cards--fancy', title: 'Fancy', description: '', collectionId: 'jpMFDInOnx', exampleContent: null }, stylesheetId: 'default_screen' }, { type: 'palette', props: { textColor: { id: 'DIkUmIk7Dn', alpha: 1 }, linkDecoration: 'underline' }, details: { id: '5nbE64vLoE', title: 'Custom', elementId: 'oYUGq-doKY', description: '', exampleContent: null }, stylesheetId: 'default_screen' }, { type: 'typography', props: { base: { fontSize: { unit: 'px', value: 12 }, lineHeight: null, letterSpacing: { unit: 'px', value: 1 }, paragraphSpacing: { unit: 'px', value: 10 } }, overrides: {}, fontVariant: { id: 'fFELCHdnc8', name: '700' }, allowBoldAndItalic: false }, details: { id: 'EZ6LSgelZE', title: 'Custom', elementId: 'oYUGq-doKY', description: '', exampleContent: 'Example' }, stylesheetId: 'default_screen' } ], formatVersion: 20, status: 'published', revision: 1, createdAt: new Date(), modifiedAt: new Date() } describe('testing stylesheets', () => { let styles = [] describe('slug generation', () => { it('should generate rule slug without collection', () => { expect( Style.Rule.getSlug( Style.Rule({ details: { title: 'Red', collectionId: 'collection-id', id: 'color-id' }, type: 'decoration' }), [ Style.Rule({ details: { title: 'Collection 123', id: 'collection-id' }, props: { type: 'color' }, type: 'collection' }) ] ) ).toEqual('red') }) it('should generate element slug without collection', () => { expect( Style.Rule.getSlug( Style.Rule({ details: { title: 'Texty', collectionId: 'collection-id', id: 'color-id' }, type: 'text' }), [ Style.Rule({ details: { title: 'Collection 123', id: 'collection-id' }, props: { type: 'text' }, type: 'collection' }) ] ) ).toEqual('collection-123--texty') }) it('should generate classname', () => { expect( Style.Rule.getClassName( Style.Rule({ details: { title: 'Red', collectionId: 'collection-id', id: 'color-id' }, type: 'color' }), [ Style.Rule({ details: { title: 'Collection 123', id: 'collection-id' }, props: { type: 'color' }, type: 'collection' }) ] ) ).toEqual('-color--collection-123--red') }) it('should generate classname for rule', () => { expect( Style.Rule.getClassName( Style.Rule({ details: { title: 'Red', collectionId: 'collection-id', id: 'decoration-id' }, type: 'decoration' }), [ Style.Rule({ details: { title: 'Super Decorations', id: 'collection-id' }, props: { type: 'decoration' }, type: 'collection' }) ] ) ).toEqual('-decoration--red') }) }) describe('Migrate stylesheet from version 0 to version 1', () => { it('should migrate section', () => { styles = [ { type: 'card', details: { id: '123', description: '', exampleContent: null, title: '' }, props: { decorationIds: ['123', '3123'] } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'block', details: { id: '123', description: '', exampleContent: null, slug: 'card--', title: '', collectionId: 'card' }, props: { decorationIds: ['123', '3123'] } }, ...Style.Presets.breakpoints ]) }) it('should migrate card', () => { styles = [ { type: 'card', details: { id: '123', description: '', exampleContent: null, title: 'Zuper big (1)' }, props: { decorationIds: ['123', '3123'] } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'block', details: { id: '123', description: '', exampleContent: null, title: 'Zuper big (1)', slug: 'card--zuper-big--1-', collectionId: 'card' }, props: { decorationIds: ['123', '3123'] } }, ...Style.Presets.breakpoints ]) }) it('should migrate button', () => { styles = [ { type: 'button', details: { id: '456', description: '', exampleContent: null, title: 'test' }, props: { decorationIds: ['123', '3123'] } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'inline', details: { title: 'test', description: '', exampleContent: null, id: '456', slug: 'button--test', collectionId: 'button' }, props: { decorationIds: ['123', '3123'] } }, ...Style.Presets.breakpoints ]) }) it('should migrate color', () => { styles = [ { type: 'color', details: { id: '12312312', description: '', exampleContent: null, title: 'Color' }, props: { colors: [ { color: { red: '123', green: 234, blue: 132, alpha: 0.33 }, name: 'Color name' }, { color: { red: '2', green: 134, blue: 132, alpha: 0.33 }, name: 'Color name2' } ] } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'collection', details: { title: 'Color', slug: 'color', id: '12312312' }, props: { type: 'color' } }, { type: 'color', details: { collectionId: '12312312', title: 'Color name', description: '', exampleContent: null, id: '12312312-Color name' }, props: { red: 123, green: 234, blue: 132, alpha: 0.33 } }, { type: 'color', details: { collectionId: '12312312', title: 'Color name2', description: '', exampleContent: null, id: '12312312-Color name2' }, props: { red: 2, green: 134, blue: 132, alpha: 0.33 } }, ...Style.Presets.breakpoints ]) }) it('should migrate fill', () => { styles = [ { type: 'fill', details: { id: '12312312', title: 'My fill', description: '', exampleContent: null }, props: { backgroundColor: { id: '123123123', name: 'old', alpha: null } } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'fill', details: { collectionId: 'default-fill', id: '12312312', title: 'My fill', slug: 'my-fill', description: '', exampleContent: null }, props: { backgroundColor: { id: '123123123-old', alpha: null } } }, ...Style.Presets.breakpoints ]) }) it('should migrate decoration', () => { styles = [ { type: 'decoration', details: { id: '12312312', title: 'My decoration', description: '', exampleContent: null }, props: { boxShadowColor: { id: '123123123', name: 'old', alpha: null } } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'decoration', details: { collectionId: 'default-decoration', id: '12312312', slug: 'my-decoration', title: 'My decoration', description: '', exampleContent: null }, props: { boxShadowColor: { id: '123123123-old', alpha: null } } }, ...Style.Presets.breakpoints ]) }) it('should migrate text p', () => { styles = [ { type: 'text', details: { id: '12312312', title: 'Paragraph style', description: '', exampleContent: null }, props: { tag: 'p' } } ] const migrated = Style.migrateStylesheets(styles, 1) expect(migrated).toEqual([ { type: 'text', details: { id: '12312312', slug: 'paragraph-style', title: 'Paragraph style', description: '', exampleContent: null, collectionId: 'paragraph' }, props: {} }, ...Style.Presets.breakpoints ]) }) }) describe('Migrate stylesheet from version 1 to version 2', () => { it('should migrate inlines', () => { styles = [ { type: 'inline', details: { id: '12312312', title: 'Paragraph style', description: '', collectionId: 'button' }, props: {} } ] const migrated = Style.migrateStylesheets(styles, 2) expect(migrated).toEqual([ { type: 'inline', details: { id: '12312312', title: 'Paragraph style', description: '', collectionId: 'button' }, props: { paletteIds: [`12312312-palette`] } }, { type: 'palette', details: { id: `12312312-palette`, description: '', elementId: '12312312', exampleContent: null, title: 'Custom' }, props: { linkDecoration: 'underline', textColor: { alpha: null, id: null } } } ]) }) describe('from 0 to 4 ', () => { it('should migrate text', () => { var styles = [ { type: 'text', details: { id: '12312312', title: 'Paragraph style', description: '', exampleContent: null }, props: { tag: 'p' } } ] as any for (var i = 1; i < 5; i++) styles = Style.migrateStylesheets(styles, i) expect(styles[0].details.slug).toEqual('text--paragraph-style') }) it('should migrate elements to theme', () => { var styles = [ { type: 'decoration', details: { id: 'decoration', title: 'decoration' }, props: {} }, { type: 'fill', details: { id: 'fill', title: 'fill' }, props: {} }, { type: 'typography', details: { id: 'typography', title: 'typography' }, props: {} }, { type: 'text', details: { id: 'text', title: 'text style', description: '', exampleContent: null, isDefault: true }, props: { isDefault: true, typographyIds: ['typography'] } }, { type: 'block', details: { id: 'block', title: 'block style', description: '', exampleContent: null, isDefault: true }, props: { isDefault: true } }, { type: 'block2', details: { id: 'block2', title: 'block style', description: '', exampleContent: null, isDefault: false }, props: { isDefault: true } } ] as any for (var i = 1; i < 5; i++) styles = Style.migrateStylesheets(styles, i) const theme = styles.find((s: any) => s.type == 'theme') expect(theme.props.texts.length).toEqual(1) expect(theme.props.texts[0].refId).toEqual('text') expect(theme.props.texts[0].typographyId).toEqual('typography') expect(theme.props.texts[0].paletteId).toEqual(null) expect(theme.props.blocks.length).toEqual(1) expect(theme.props.blocks[0].refId).toEqual('block') expect(theme.props.blocks[0].fillId).toEqual('fill') expect(theme.props.blocks[0].spacingId).toEqual('blank-spacing') }) }) describe('from 5 to 6 ', () => { it('should fix xxlarge breakpoint', () => { var styles = [ { type: 'breakpoint', details: { id: 'xxlarge', title: 'Extra Extra Large' }, props: { query: 'something', minWidth: 100, maxWidth: 0 } } ] styles = Style.migrateStylesheets(styles, 6) expect(styles).toEqual([ { type: 'breakpoint', details: { id: 'xxlarge', title: 'Extra Extra Large' }, props: { query: 'something', minWidth: 100, maxWidth: Infinity } } ]) }) }) describe('from 7 to 8 ', () => { it('should create link styles', () => { var styles = [ { details: { id: 'default-theme', slug: 'default', title: 'Default theme' }, props: { blocks: [], inlines: [], texts: [] }, type: 'theme' }, { type: 'text', details: { id: 'my-heading', collectionId: 'heading1' }, props: { paletteIds: ['heading-palette'] } }, { type: 'palette', details: { id: 'heading-palette' }, props: { linkColor: { id: 'link-color', alpha: 0.99 }, textColor: { id: 'text-color', alpha: 0.99 }, linkDecoration: 'dotted' } } ] as any styles = Style.migrateStylesheets(styles, 8) expect(styles).toEqual([ { details: { id: 'default-theme', slug: 'default', title: 'Default theme' }, props: { blocks: [], inlines: [ { id: styles.find((s: any) => s.type == 'theme')?.props.inlines[0].id, isDefault: true, refId: 'default-link', paletteId: 'link-palette', decorationId: 'blank-decoration', typographyId: 'blank-typography', spacingId: 'blank-spacing', fillId: 'blank-fill' } ], texts: [] }, type: 'theme' }, { type: 'text', details: { id: 'my-heading', collectionId: 'heading1' }, props: { paletteIds: ['heading-palette'] } }, { type: 'palette', details: { id: 'heading-palette' }, props: { linkColor: { id: 'link-color', alpha: 0.99 }, // will be normalized after that textColor: { id: 'text-color', alpha: 0.99 }, linkDecoration: 'dotted' } }, { type: 'inline', details: { id: 'default-link', collectionId: 'link', description: 'Typical link style', title: 'Default link' }, props: { paletteIds: ['link-palette'], decorationId: ['blank-decoration'], typographyId: ['blank-typography'], spacingId: ['blank-spacing'], fillId: ['blank-fill'] } }, { type: 'palette', details: { id: 'link-palette', elementId: 'default-link', title: 'Default link palette' }, props: { textColor: { id: 'link-color', alpha: 0.99 }, linkDecoration: 'dotted' } } ]) }) }) }) describe('Fix styles', () => { const paletteId = 'palette-id' const blankPaletteId = 'blank-palette' const decorationId = 'decoration-id' const blankDecorationId = 'blank-decoration' const fillId = 'fill-id' const blankFillId = 'blank-fill' const spacingId = 'spacing-id' const blankSpacingId = 'blank-spacing' const typographyId = 'typography-id' const blankTypographyId = 'blank-typography' const blankGraphic = 'blank-graphic' const blockId = 'block-id' const inlineId = 'inline-id' const textId = 'text-id' const blankColorId = 'blank-color' const fontId = 'text-id' const variantName1 = 'variant-name1' const variantName2 = 'variant-name2' const checkFix = (type: Style.Rule['type'], props: any, expected: object): void => { const normalizedProps = Style.TypeDefinitions[type].Props(props) const fixed = Style.Props.fix(type, normalizedProps, styles) const normalizedExpectation = Style.TypeDefinitions[type].Props(expected) expect(fixed).toEqual(normalizedExpectation) } let styles = [ ...Style.Presets.internal, Style.Rule({ type: 'decoration', details: { id: decorationId }, props: Style.Decoration.Props({}) }), Style.Rule({ type: 'fill', details: { id: fillId }, props: Style.Fill.Props({}) }), Style.Rule({ type: 'grid', details: { id: 'grid-id' }, props: {} }), Style.Rule({ type: 'layout', details: { id: 'layout-id' }, props: Style.Layout.Props({}) }), Style.Rule({ type: 'palette', details: { id: paletteId }, props: Style.Palette.Props({}) }), Style.Rule({ type: 'font', details: { id: fontId }, props: Style.Font.Props({ variants: [{ name: variantName1 }, { name: variantName2 }] }) }), Style.Rule({ type: 'color', details: { id: 'color-1' }, props: Style.Color.Props({}) }), Style.Rule({ type: 'block', details: { id: blockId, collectionId: 'card' }, props: { decorationIds: [decorationId], fillIds: [fillId], spacingIds: [blankSpacingId] } }), Style.Rule({ type: 'block', details: { id: 'block-id-2', collectionId: 'card' }, props: { decorationIds: [decorationId], fillIds: [], spacingIds: [blankSpacingId] } }), Style.Rule({ type: 'inline', details: { id: inlineId, collectionId: 'badge' }, props: {} }), Style.Rule({ type: 'text', details: { id: textId, collectionId: 'heading1' }, props: {} }), Style.Rule({ type: 'theme', details: { id: 'theme-id' }, props: { blocks: [fillId], inlines: [blankDecorationId], texts: [spacingId] } }) ] it('should fix decoration', () => { checkFix( 'decoration', { borderColor: { id: 'non-existent-id', alpha: 0.99 }, borderStyle: 'solid', borderLeftWidth: 3, boxShadowInset: false, boxShadowOffsetX: 5, boxShadowSpreadRadius: 5, boxShadowColor: { id: 'color-1', alpha: 0.99 } }, { borderColor: { id: blankColorId, alpha: 0.99 }, borderStyle: 'solid', borderLeftWidth: Style.Length(3), boxShadowOffsetX: Style.Length(5), boxShadowSpreadRadius: Style.Length(5), boxShadowColor: { id: 'color-1', alpha: 0.99 } } ) }) describe('Fill', () => { it('should fix fill', () => { checkFix( 'fill', { backgroundColor: { id: 'no', alpha: 0.2 }, backgroundGradient: { angle: 5, stops: [ { color: { id: 'no-color', alpha: 0.33 }, start: 3, end: 2 }, { color: { id: 'color-1', alpha: 0.3 }, start: 2, end: 3 } ] } }, { backgroundColor: { id: blankColorId, alpha: 0.2 }, backgroundGradient: { angle: 5, stops: [ { color: { id: blankColorId, alpha: 0.33 }, start: 3, end: 2 }, { color: { id: 'color-1', alpha: 0.3 }, start: 2, end: 3 } ] } } ) }) }) describe('Fix typography', () => { it('should fix typography with correct variant reference (correct font name and correct variant name)', () => { checkFix( 'typography', { fontVariant: { id: fontId, name: variantName1 } }, { fontVariant: { id: fontId, name: variantName1 } } ) }) it('should fix typography with incorrect variant reference (incorrect font name and correct variant name)', () => { checkFix('typography', { fontVariant: { id: 'wrong font name', name: variantName1 } }, { fontVariant: null }) }) it('should fix typography with incorrect variant reference (correct font name and incorrect variant name)', () => { checkFix( 'typography', { fontVariant: { id: fontId, name: 'wrong variant name' } }, { fontVariant: { id: fontId, name: variantName1 } } ) }) }) describe('Fix block', () => { it('should fix block with correct data', () => { checkFix( 'block', { decorationIds: [decorationId, '2', '3'], fillIds: ['5', fillId], spacingIds: [blankSpacingId], gridIds: ['grid-id'], layoutIds: [undefined, null, 'other-layout'] }, { decorationIds: [decorationId], fillIds: [fillId], spacingIds: [blankSpacingId], gridIds: ['grid-id'], layoutIds: ['blank-layout'], paletteIds: [] } ) }) }) it('should fix inline', () => { checkFix( 'inline', { decorationIds: [], typographyIds: ['333'], fillIds: ['5', fillId, blankFillId], spacingIds: [blankSpacingId, 'new-spacing'], paletteIds: ['layout-id', 'other-layout', paletteId] }, { decorationIds: [], typographyIds: [blankTypographyId], fillIds: [fillId, blankFillId], graphicIds: [], spacingIds: [blankSpacingId], paletteIds: [paletteId] } ) }) it('should fix text', () => { checkFix( 'text', { typographyIds: ['333'], paletteIds: ['layout-id', 'other-layout', paletteId] }, { typographyIds: [blankTypographyId], paletteIds: [paletteId] } ) }) it('should fix theme', () => { checkFix( 'theme', { blocks: [ { id: 'id', refId: blockId, isDefault: true, fillId: '23' }, { id: 'id', refId: 'block-id-2', isDefault: true, fillId: 'not-existent' } ], texts: [], inlines: [] }, { blocks: [ { id: 'id', refId: blockId, isDefault: false, fillId }, { id: 'id', refId: 'block-id-2', isDefault: true, fillId: blankFillId } ] } ) }) }) it('it should copy block', () => { const paletteId = 'palette-id' const blankPaletteId = 'blank-palette' const decorationId = 'decoration-id' const customDecorationId = 'custom-decoration-id' const blankDecorationId = 'blank-decoration' const fillId = 'fill-id' const blankFillId = 'blank-fill' const spacingId = 'spacing-id' const blankSpacingId = 'blank-spacing' const typographyId = 'typography-id' const blankTypographyId = 'blank-typography' const blankGraphic = 'blank-graphic' const blockId = 'block-id' let styles: any = [ ...Style.Presets.internal, { type: 'decoration', details: { id: decorationId, title: 'a' }, props: Style.Decoration.Props({}) }, { type: 'fill', details: { id: fillId, title: 'a' }, props: Style.Fill.Props({}) }, { type: 'grid', details: { id: 'grid-id', title: 'a' }, props: {} }, { type: 'layout', details: { id: 'layout-id', title: 'a' }, props: Style.Layout.Props({}) }, { type: 'palette', details: { id: paletteId, title: 'a' }, props: Style.Palette.Props({}) }, { type: 'decoration', details: { elementId: blockId, id: customDecorationId, title: 'a' }, props: Style.Decoration.Props({}) }, { type: 'block', details: { id: blockId, title: 'Title' }, props: { decorationIds: [decorationId, customDecorationId], fillIds: [fillId], spacingIds: [blankSpacingId] } } ] const style = styles.find((s: any) => s.type === 'block') const [newStyle, ...newStyles] = StylesheetModel.getDuplicateRule(style, styles) const custom = newStyles.find((s) => s.type == 'decoration') expect(newStyle.details.title).toEqual('Title (1)') expect((newStyle as Style.Rule<'block'>).props.decorationIds).to.not.contain(customDecorationId) expect((newStyle as Style.Rule<'block'>).props.decorationIds).to.contain(custom.details.id) expect(newStyles).toEqual([custom]) }) it('it should copy theme', () => { const paletteId = 'palette-id' const blankPaletteId = 'blank-palette' const decorationId = 'decoration-id' const customDecorationId = 'custom-decoration-id' const blankDecorationId = 'blank-decoration' const fillId = 'fill-id' const blankFillId = 'blank-fill' const spacingId = 'spacing-id' const blankSpacingId = 'blank-spacing' const typographyId = 'typography-id' const blankTypographyId = 'blank-typography' const blankGraphic = 'blank-graphic' const blockId = 'block-id' let styles: any = [ ...Style.Presets.internal, { type: 'decoration', details: { id: decorationId, title: 'a' }, props: Style.Decoration.Props({}) }, { type: 'fill', details: { id: fillId, title: 'a' }, props: Style.Fill.Props({}) }, { type: 'grid', details: { id: 'grid-id', title: 'a' }, props: {} }, { type: 'layout', details: { id: 'layout-id', title: 'a' }, props: Style.Layout.Props({}) }, { type: 'palette', details: { id: paletteId, title: 'a' }, props: Style.Palette.Props({}) }, { type: 'decoration', details: { elementId: blockId, id: customDecorationId, title: 'a' }, props: Style.Decoration.Props({}) }, { type: 'block', details: { id: blockId, title: 'Title', collectionId: 'card' }, props: { decorationIds: [decorationId, customDecorationId], fillIds: [fillId], spacingIds: [blankSpacingId] } }, { type: 'theme', details: { id: 'theme-id', title: 'Title' }, props: { inlines: [], texts: [], blocks: [{ id: '1', refId: blockId, decorationId, fillId, spacingId }] } } ] const style = styles.find((s: any) => s.type === 'theme') const [newStyle] = StylesheetModel.getDuplicateRule(style, styles) expect((newStyle as Style.Rule<'theme'>).props.blocks[0].id).not.to.eq(1) }) it('should migrate theme to 9', () => { styles = [ { type: 'theme', details: { id: '1' }, props: { inlines: [{ id: '1' }], texts: [{ id: '2' }], blocks: [{ id: '3' }] } }, { type: 'theme', details: { id: '2' }, props: { inlines: [{ id: '1' }], texts: [{ id: '2' }], blocks: [{ id: '3' }] } } ] const migrated = Style.migrateStylesheets(styles, 9) expect(migrated[1].props.inlines[0].id).to.not.eq('1') expect(migrated[1].props.blocks[0].id).to.not.eq('1') }) it('should migrate theme to 10', () => { styles = [ { type: 'theme', details: { id: '1' }, props: { inlines: [{ id: '1' }], texts: [{ id: '4' }], blocks: [{ id: '3' }] } }, { type: 'theme', details: { id: '2' }, props: { inlines: [{ id: '12' }], texts: [{ id: '4' }], blocks: [{ id: '14' }] } } ] const migrated = Style.migrateStylesheets(styles, 10) expect(migrated[1].props.texts[0].id).to.not.eq('4') expect(migrated[0].props.text).to.not.exist expect(migrated[1].props.text).to.not.exist }) it('should migrate theme to 11', () => { styles = [ { type: 'breakpoint', details: { id: '1' }, props: { minWidth: 0, maxWidth: 576, query: '(max-width: 576px)' } } ] const migrated = Style.migrateStylesheets(styles, 11) expect(migrated[0].props.maxWidth).to.eq(575) expect(migrated[0].props.query).to.eq('(max-width: 575px)') }) it('should migrate theme to 12', () => { styles = [ { type: 'theme', details: { id: '1', title: '1' }, props: { texts: [ { refId: '5', isDefault: false, id: '55' }, { refId: '5', isDefault: false, id: '55' } ], blocks: [ { refId: '2', isDefault: true, id: '55' }, { refId: '2', isDefault: true, id: '55' }, { refId: '2', isDefault: false, id: '55' } ], inlines: [] } }, { type: 'text', details: { title: '5', id: '5', collectionId: 'heading1' }, props: Style.Text.Props({}) }, { type: 'block', details: { title: '2', id: '2', collectionId: 'section' }, props: Style.Block.Props({}) }, ...Style.Presets.internal ] const migrated = Style.migrateStylesheets(styles, 12) console.log({ migrated: migrated[0].props }) expect(migrated[0].props.texts[0].isDefault).to.eq(true) expect(migrated[0].props.texts[1].isDefault).to.eq(false) expect(migrated[0].props.blocks[0].isDefault).to.eq(false) expect(migrated[0].props.blocks[1].isDefault).to.eq(true) expect(migrated[0].props.blocks[2].isDefault).to.eq(false) }) it('should migrate theme to 13', () => { styles = [ { type: 'text', details: { id: '5', collectionId: 'heading2' }, props: Style.Text.Props({}) }, { type: 'dimensions', details: { id: '2', collectionId: '65' }, props: Style.Dimensions.Props({ minWidth: 0, minHeight: 1, maxHeight: 0, maxWidth: 2 }) } ] const migrated = Style.migrateStylesheets(styles, 13) expect(migrated[0].props).toEqual(Style.Text.Props({})) expect(migrated[1].props).toEqual({ minWidth: null, minHeight: { value: 1, unit: 'px' }, maxWidth: { value: 2, unit: 'px' }, maxHeight: null }) }) it.skip('removes untitled', () => { styles = [ { type: 'text', details: { id: '5', collectionId: '65' }, props: Style.Text.Props({}) }, { type: 'dimensions', details: { id: '2', title: 'a', collectionId: '65' }, props: Style.Dimensions.Props({ minWidth: 0, minHeight: 1, maxHeight: 0, maxWidth: 2 }) }, { type: 'font', details: { id: '2', collectionId: '65' }, props: Style.Font.Props({ familyName: 'dqwd' }) }, { type: 'font', details: { id: '2', collectionId: '65', title: 'teeest' }, props: Style.Font.Props({}) } ] const migrated = Style.migrateStylesheets(styles, 17) expect(migrated.length).toEqual(2) }) it('renames xsmall', () => { styles = [ { type: 'typography', props: { base: { fontSize: { unit: 'px', value: 42 }, lineHeight: { unit: 'px', value: 54 }, letterSpacing: { unit: 'px', value: 3 }, paragraphSpacing: { unit: 'px', value: 30 } }, overrides: { small: { fontSize: { unit: 'px', value: 32 }, lineHeight: { unit: 'px', value: 38 }, letterSpacing: { unit: 'px', value: 3 }, paragraphSpacing: { unit: 'px', value: 20 } }, 'extra small': { fontSize: { unit: 'px', value: 32 }, lineHeight: { unit: 'px', value: 38 }, letterSpacing: { unit: 'px', value: 3 }, paragraphSpacing: { unit: 'px', value: 20 } } }, fontVariant: { id: 'fFELCHdnc8', name: '700' }, allowBoldAndItalic: false }, details: { id: 'PVgC6H-uGW', slug: 'default-typographies--large-decorative', title: 'Large decorative', description: '', collectionId: 'default-typography', exampleContent: 'Example' }, stylesheetId: 'default_screen' } ] const migrated = Style.migrateStylesheets(styles, 23) expect(migrated[0].props.overrides['extra small']).toBeUndefined() expect(migrated[0].props.overrides['xsmall']).toEqual({ fontSize: { unit: 'px', value: 32 }, lineHeight: { unit: 'px', value: 38 }, letterSpacing: { unit: 'px', value: 3 }, paragraphSpacing: { unit: 'px', value: 20 } }) }) it('fix breakpoint query precision', () => { styles = [ { type: 'breakpoint', details: { id: 'xxlarge', title: 'Extra Extra Large' }, props: { query: '(min-width: 0px) and (max-width: 300.999px)', minWidth: 0, maxWidth: 300 } } ] const migrated = Style.migrateStylesheets(styles, 30) expect(migrated[0].props.query).toBe('(min-width: 0px) and (max-width: 300.98px)') }) describe('Stylesheet model', () => { const library = new LibraryModel() let spy: SpyInstance beforeEach(() => { spy = vi.spyOn(Transport, 'fetch') // @ts-ignore spy.mockImplementation(async (...args: Parameters) => { return new Response(args[1].body) }) }) var ruleCount = 0 describe('add stylesheet', () => { it('should add new stylesheets', () => { expect(library.stylesheets.length).toEqual(0) library.stylesheets.upsertItem(testStyle) ruleCount = library.stylesheets[0].rules.length expect(library.stylesheets.length).toEqual(1) expect(library.stylesheets[0].status).toEqual('published') }) it('should create new draft', () => { library.stylesheets[0].getDraft() expect(library.stylesheets.length).toEqual(2) }) it('should order new draft as first stylesheet', () => { expect(library.stylesheets[0].status).toEqual('draft') }) }) it('should add new rule', () => { library.stylesheets[0].upsertRule({ type: 'fill', props: { blur: { unit: 'px', value: 0 }, opacity: 1, // @ts-ignore backgroundSize: 'contain', backgroundColor: { id: 'j1vh5Qzhgs', alpha: 1 }, backgroundImage: '', backgroundGradient: { angle: 0, stops: [] }, backgroundPositionX: { unit: '%', value: 50 }, backgroundPositionY: { unit: '%', value: 50 } }, details: { id: 'fSFU_LKxjn', slug: 'default-background--dark', title: 'Dark', description: '', collectionId: '1Uf9MB-XyG', exampleContent: null } }) expect(library.stylesheets[0].rules.length).toBeGreaterThan(library.stylesheets[1].rules.length) expect(library.stylesheets[0].rules.length).toEqual(ruleCount + 1) }) it('should delete rule', () => { library.stylesheets[0].deleteRule({ details: { title: '', id: 'fFELCHdnc8' } } as Style.Rule<'fill'>) expect(library.stylesheets[0].rules.length).toEqual(library.stylesheets[1].rules.length) expect(library.stylesheets[0].rules.length).toEqual(ruleCount) }) it('should save stylesheet', () => { library.stylesheets[0].saveDraft() expect(spy).toHaveBeenCalledTimes(1) expect(library.stylesheets.length).toEqual(3) expect(library.stylesheets[0].status).toEqual('saved') expect(library.stylesheets[0].rules.length).toEqual(ruleCount) }) it('should stage stylesheet', () => { library.stylesheets[0].stage() expect(spy).toHaveBeenCalledTimes(1) expect(library.stylesheets.length).toEqual(4) expect(library.stylesheets[0].status).toEqual('staged') expect(library.stylesheets[0].rules.length).toEqual(ruleCount) }) it('should publish stylesheet', () => { library.stylesheets[0].publish() expect(spy).toHaveBeenCalledTimes(1) expect(library.stylesheets.length).toEqual(5) expect(library.stylesheets[0].status).toEqual('published') expect(library.stylesheets[0].rules.length).toEqual(ruleCount) }) it('should check if two stylesheets are the same ', () => { library.stylesheets[0].deleteRule({ details: { title: '', id: 'fSFU_LKxjn' } } as Style.Rule<'fill'>) library.stylesheets[0].saveDraft() expect(spy).toHaveBeenCalledTimes(1) expect(library.stylesheets[0].getCurrent('published').isUpToDateWith('staged')).toBeTruthy() expect(library.stylesheets[0].isUpToDateWith('staged')).toBeFalsy() expect(library.stylesheets[0].isUpToDateWith('published')).toBeFalsy() }) it('should rollback draft to published', () => { library.stylesheets[0].getCurrent('published').revertDraftTo() expect(spy).toHaveBeenCalledTimes(1) expect(library.stylesheets[0].isUpToDateWith('published')).toBeTruthy() }) it('should stage on publishing', async () => { library.stylesheets[0].upsertRule({ type: 'font', props: { platform: 'google' as Platform, variants: [{ name: 'regular', style: 'normal' as VariantStyle, weight: 400, familyName: 'Nunito Sans' }], familyName: 'Nunito Sans' }, details: { id: 'Rfdgvfds', slug: 'test', title: 'test', description: 'test', exampleContent: null } }) expect(library.stylesheets[0].status).toEqual('draft') await library.stylesheets[0].publish() expect(spy).toHaveBeenCalledTimes(2) expect(library.stylesheets[0].status).toEqual('published') expect(library.stylesheets[1].status).toEqual('staged') expect(library.stylesheets.length).toEqual(11) }) it('can get latest stylesheet for given status', () => { const latestDraft = library.stylesheets[0].getCurrent('draft') const latestSaved = library.stylesheets[0].getCurrent('saved') const latestStaged = library.stylesheets[0].getCurrent('staged') const latestPublished = library.stylesheets[0].getCurrent('published') expect( library.stylesheets.filter((s) => s.status === 'draft').every((s) => s.revision <= latestDraft.revision) ).toBeTruthy() expect( library.stylesheets.filter((s) => s.status === 'saved').every((s) => s.revision <= latestSaved.revision) ).toBeTruthy() expect( library.stylesheets.filter((s) => s.status === 'staged').every((s) => s.revision <= latestStaged.revision) ).toBeTruthy() expect( library.stylesheets.filter((s) => s.status === 'published').every((s) => s.revision <= latestPublished.revision) ).toBeTruthy() }) it('provides default font names', () => { expect( Style.migrateStylesheets([{ type: 'font', details: { id: 'abc' }, props: { familyName: 'test' } }], 28, true)[0] ).toEqual({ details: { id: 'abc', title: 'test' }, props: { familyName: 'test' }, type: 'font' }) expect(Style.migrateStylesheets([{ type: 'font', details: { id: 'abc' } }], 28, true)[0]).toEqual({ details: { id: 'abc', title: 'Unknown font' }, type: 'font' }) }) }) describe('should copy stylesheet', () => { it('to empty library', async () => { const spy = vi.spyOn(StylesheetModel.prototype, 'post') spy.mockImplementation(function (this: StylesheetModel, data: any) { return Promise.resolve(this) }) const from = new LibraryModel() from.stylesheets.add({ revision: 3, status: 'saved', source: [ { type: 'font', details: { id: 'test', title: 'abc' }, props: { familyName: 'Open Sans', platform: 'google' } } ] }) const to = new LibraryModel() await from.stylesheet.copyToLibrary(to) expect(to.stylesheet.source).to.eql(from.stylesheet.source) expect(to.stylesheet.revision).to.eql(2) expect(to.stylesheet.status).to.eql('saved') }) it('to library with stylesheet with higher revision', async () => { const spy = vi.spyOn(StylesheetModel.prototype, 'post') spy.mockImplementation(function (this: StylesheetModel, data: any) { return Promise.resolve(this) }) const from = new LibraryModel() from.stylesheets.add({ revision: 3, status: 'saved', source: [ { type: 'font', details: { id: 'test', title: 'abc' }, props: { familyName: 'Open Sans', platform: 'google' } } ] }) const to = new LibraryModel() to.stylesheets.add({ revision: 7, status: 'saved' }) await from.stylesheet.copyToLibrary(to) expect(to.stylesheet.source).to.eql(from.stylesheet.source) expect(to.stylesheet.revision).to.eql(10) expect(to.stylesheet.status).to.eql('saved') }) it('to library with stylesheet with lower revision', async () => { const spy = vi.spyOn(StylesheetModel.prototype, 'post') spy.mockImplementation(function (this: StylesheetModel, data: any) { return Promise.resolve(this) }) const from = new LibraryModel() from.stylesheets.add({ revision: 7, status: 'saved', source: [ { type: 'font', details: { id: 'test', title: 'abc' }, props: { familyName: 'Open Sans', platform: 'google' } } ] }) const to = new LibraryModel() to.stylesheets.add({ revision: 3, status: 'saved' }) await from.stylesheet.copyToLibrary(to) expect(to.stylesheet.source).to.eql(from.stylesheet.source) expect(to.stylesheet.revision).to.eql(6) expect(to.stylesheet.status).to.eql('saved') }) }) })