UNPKG

@sketch-hq/sketch-assistant-utils

Version:

Utility functions and types for Sketch Assistants.

255 lines (254 loc) 12.9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { FileFormat, } from '@sketch-hq/sketch-assistant-types'; import { fromFile } from '@sketch-hq/sketch-file'; import { resolve } from 'path'; import { createIterable, createIterableObjectCache, createRuleUtilsCreator } from '..'; import { getImageMetadata } from '../../get-image-metadata'; import { createEmptyObjectCache, process } from '../../process'; import { createAssistantConfig, createAssistantDefinition, createDummyRect, createDummySwatch, createRule, } from '../../test-helpers'; /** * Test helper function for creating a utils object. */ const createUtils = (filepath = './empty.sketch', assistant = createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true } } }), rules: [createRule({ name: 'foo' })], }), ruleName = 'foo', violations = [], ignoreConfig = { pages: [], assistants: {} }, timeoutToken = { timedOut: false }) => __awaiter(void 0, void 0, void 0, function* () { const op = { cancelled: false }; const file = yield fromFile(resolve(__dirname, filepath)); const processedFile = yield process(file, op); return { processedFile, violations, utils: createRuleUtilsCreator(processedFile, violations, assistant, op, getImageMetadata, ignoreConfig)(ruleName, timeoutToken), }; }); describe('createIterable', () => { test('can yield file format objects', () => { const iterable = createIterable([createDummyRect(), createDummyRect()], { cancelled: false, }, { timedOut: false }, []); expect([...iterable].map((item) => item._class)).toMatchInlineSnapshot(` Array [ "rect", "rect", ] `); }); test('can short-circuit when operation is cancelled', () => { const iterable = createIterable([createDummyRect(), createDummyRect()], { cancelled: true, }, { timedOut: false }, []); expect([...iterable]).toHaveLength(0); }); }); describe('createIterableObjectCache', () => { test('works with an empty cache', () => { const cache = createEmptyObjectCache(); const iterableCache = createIterableObjectCache(cache, { cancelled: false }, { timedOut: false }, []); expect([...iterableCache.text]).toHaveLength(0); }); test('for..of loops work with type safety', () => { const cache = createEmptyObjectCache(); cache[FileFormat.ClassValue.Rect] = [createDummyRect(), createDummyRect()]; const iterableCache = createIterableObjectCache(cache, { cancelled: false }, { timedOut: false }, []); for (const rect of iterableCache.rect) { expect(rect._class).toBe('rect'); } }); test('objects can be ignored by id', () => { const cache = createEmptyObjectCache(); cache[FileFormat.ClassValue.Swatch] = [createDummySwatch('1'), createDummySwatch('2')]; const iterableCache = createIterableObjectCache(cache, { cancelled: false }, { timedOut: false }, ['2']); const result = [...iterableCache.swatch]; expect(result).toHaveLength(1); expect(result[0]).toHaveProperty('do_objectID', '1'); }); }); describe('createRuleUtilsCreator', () => { test('throws when named rule not present in the assistant', () => __awaiter(void 0, void 0, void 0, function* () { try { yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true } } }), rules: [], }), 'foo'); } catch (error) { expect(error).toMatchInlineSnapshot(`[RuleNotFoundError: Rule "foo" not found on assistant "dummy-assistant"]`); } })); }); describe('getOption', () => { test('can get an option', () => __awaiter(void 0, void 0, void 0, function* () { expect.assertions(1); const { utils } = yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true } } }), rules: [createRule({ name: 'foo' })], }), 'foo'); expect(utils.getOption('active')).toBeTruthy(); })); test('throws when option missing in config', () => __awaiter(void 0, void 0, void 0, function* () { expect.assertions(1); const { utils } = yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true } } }), rules: [createRule({ name: 'foo' })], }), 'foo'); try { utils.getOption('fluxCapacitance'); } catch (error) { expect(error).toMatchInlineSnapshot(`[InvalidRuleConfigError: Invalid configuration found for rule "foo" on assistant "dummy-assistant": Option "fluxCapacitance" not found in assistant configuration]`); } })); test("throws when option in config doesn't match rule's schema", () => __awaiter(void 0, void 0, void 0, function* () { expect.assertions(1); const { utils } = yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true, custom: '1' } } }), rules: [ createRule({ name: 'foo', getOptions: (helpers) => [ helpers.numberOption({ name: 'custom', title: '', description: '' }), ], }), ], }), 'foo'); try { utils.getOption('custom'); } catch (error) { expect(error).toMatchInlineSnapshot(`[InvalidRuleConfigError: Invalid configuration found for rule "foo" on assistant "dummy-assistant": "/custom" must be number]`); } })); }); describe('getObjectParents', () => { test('returns parent objects to the root', () => __awaiter(void 0, void 0, void 0, function* () { const { utils, processedFile } = yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true } } }), rules: [createRule({ name: 'foo' })], }), 'foo'); const style = processedFile.original.contents.document.pages[0].style; const parents = utils.getObjectParents(style); expect(parents[0]).toBe(processedFile.original.contents); expect(parents[1]).toBe(processedFile.original.contents.document); expect(parents[2]).toBe(processedFile.original.contents.document.pages); expect(parents[3]).toBe(processedFile.original.contents.document.pages[0]); })); }); describe('evalPointer', () => { test('can resolve file objects by json pointer', () => __awaiter(void 0, void 0, void 0, function* () { const { utils, processedFile } = yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true, custom: '1' } } }), rules: [createRule({ name: 'foo' })], }), 'foo'); const page = utils.evalPointer('/document/pages/0'); expect(page).toBe(processedFile.original.contents.document.pages[0]); })); }); describe('getObjectParent', () => { test('can resolve file object parents by json pointer', () => __awaiter(void 0, void 0, void 0, function* () { const { utils, processedFile } = yield createUtils(); const parent = utils.getObjectParent(processedFile.original.contents.document); expect(parent).toBe(processedFile.original.contents); })); }); describe('objects', () => { test('exposes iterators for file objects', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils(); const pages = [...utils.objects.page]; expect(pages).toHaveLength(1); expect(pages[0]._class).toBe('page'); })); }); describe('isObjectIgnored', () => { test('returns ignore status of object and rule combinations', () => __awaiter(void 0, void 0, void 0, function* () { const swatch1 = createDummySwatch('1'); const swatch2 = createDummySwatch('2'); const { utils } = yield createUtils('./empty.sketch', createAssistantDefinition({ name: 'foo', rules: [createRule({ name: 'bar' })], }), 'bar', [], { pages: [], assistants: { foo: { rules: { bar: { objects: ['1'] } } } } }); expect(utils.isObjectIgnored(swatch1)).toBe(true); expect(utils.isObjectIgnored(swatch2)).toBe(false); })); }); describe('foreignObjects', () => { test('exposes iterators for foreign file objects', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils('./foreign-symbol.sketch'); const symbols = [...utils.foreignObjects.symbolMaster]; expect(symbols).toHaveLength(2); expect(symbols[0]._class).toBe(FileFormat.ClassValue.SymbolMaster); })); }); describe('report', () => { test('can report violations', () => __awaiter(void 0, void 0, void 0, function* () { const { utils, violations } = yield createUtils(); utils.report("Something isn't right", ...utils.objects.page); expect(violations).toHaveLength(1); expect(violations[0]).toMatchInlineSnapshot(` Object { "assistantName": "dummy-assistant", "message": "Something isn't right", "objects": Array [ Object { "class": "page", "id": "9AD22B94-A05B-4F49-8EDD-A38D62BD6181", "name": "Page 1", "pointer": "/document/pages/0", }, ], "ruleName": "foo", "severity": 3, } `); })); }); describe('objectHash', () => { test('objects with the same contents produce the same hash', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils(); expect(utils.objectHash({ foo: 'bar' })).toBe(utils.objectHash({ foo: 'bar' })); })); test('property order does not matter', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils(); expect(utils.objectHash({ foo: 'bar', baz: 'qux' })).toBe(utils.objectHash({ baz: 'qux', foo: 'bar' })); })); test('do_objectID values are ignored by default', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils(); expect(utils.objectHash({ foo: 'bar', do_objectID: '1' })).toBe(utils.objectHash({ foo: 'bar', do_objectID: '2' })); })); }); describe('styleEq', () => { test('can test style objects for equality', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils('./empty.sketch', createAssistantDefinition({ config: createAssistantConfig({ rules: { foo: { active: true } } }), rules: [createRule({ name: 'foo' })], }), 'foo'); const file1 = yield fromFile(resolve(__dirname, './simple-style.sketch')); const file2 = yield fromFile(resolve(__dirname, './simple-style.sketch')); const style1 = file1.contents.document.pages[0].layers[0].style; const style2 = file2.contents.document.pages[0].layers[0].style; if (style2 && style2.blur) style2.blur.center = '{0.6, 0.5}'; expect(utils.styleEq(style1, style1)).toBe(true); expect(utils.styleEq(style1, style2)).toBe(false); })); test('can test text style objects for equality', () => __awaiter(void 0, void 0, void 0, function* () { const { utils } = yield createUtils(); const file1 = yield fromFile(resolve(__dirname, './simple-textstyle.sketch')); const file2 = yield fromFile(resolve(__dirname, './simple-textstyle.sketch')); const style1 = file1.contents.document.pages[0].layers[0].style; const style2 = file2.contents.document.pages[0].layers[0].style; expect(utils.textStyleEq(style1, style1)).toBe(true); if (style2 && style2.textStyle) { style2.textStyle.encodedAttributes.underlineStyle = 1; } expect(utils.textStyleEq(style1, style2)).toBe(false); })); });