@sketch-hq/sketch-assistant-utils
Version:
Utility functions and types for Sketch Assistants.
255 lines (254 loc) • 12.9 kB
JavaScript
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);
}));
});