assert#ok TypeScript Examples

The following examples show how to use assert#ok. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: authorization-request-manager.ts    From malagu with MIT License 6 votes vote down vote up
async save(authorizationRequest: AuthorizationRequest): Promise<void> {
        if (authorizationRequest) {
            const state = authorizationRequest.state;
            ok(state, 'authorizationRequest.state cannot be empty');
            const authorizationRequests = this.getAuthorizationRequests();
            authorizationRequests[state] = authorizationRequest;
            const session = Context.getSession();
            session[this.sessionAttributeName] = authorizationRequests;
        } else {
            await this.remove();
        }
    }
Example #2
Source File: refresh-token-authorized-client-provider.ts    From malagu with MIT License 6 votes vote down vote up
async provide(context: AuthorizationContext): Promise<AuthorizedClient | undefined> {
        const { clientRegistration, authorizedClient, attributes } = context;
        if (!authorizedClient || !authorizedClient.refreshToken || !this.hasTokenExpired(authorizedClient.accessToken)) {
            return;
        }

        const scopes = attributes.get(AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);

        ok(scopes || Array.isArray(scopes), `The context attribute must be of type Array '${AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME}'`);

        const tokenResponse = await this.getTokenResponse(clientRegistration, authorizedClient, scopes);
        return {
            clientRegistration,
            principalName: context.principal.name,
            accessToken: tokenResponse.accessToken,
            refreshToken: tokenResponse.refreshToken
        };
    }
Example #3
Source File: init-manager.ts    From malagu with MIT License 6 votes vote down vote up
protected async selectTemplate(): Promise<void> {
        const { template } = this.opts;
        if (template) {
            this.templateName = template;
            if (remoteTemplateRegex.test(template)) {
                this.location = template;
            } else {
                Object.keys(templates).forEach(key => {
                    if (key === template) {
                        this.location = templates[key];
                    }
                });
                ok(this.location, `"${template}" template not found`);
            }
            return;
        }
        const spinner = ora({ text: 'loading...', discardStdin: false }).start();
        const answers = await inquirer.prompt([{
            name: 'item',
            type: 'autocomplete',
            pageSize: 12,
            message: 'Select a template to init',
            source: async (answersSoFar: any, input: string) => {
                if (!this.source) {
                    const officialTemplates = Object.keys(templates).map(key => this.toOfficialTemplate(key, templates[key]));
                    this.source = officialTemplates;
                    spinner.stop();
                }
                return this.source.filter(item => !input || item.name.toLowerCase().includes(input.toLowerCase()));
            }
        }]);
        this.templateName = answers.item.name;
        this.location = answers.item.location;
    }
Example #4
Source File: proxy-generator.ts    From prisma-nestjs-graphql with MIT License 6 votes vote down vote up
generatorHandler({
  async onGenerate(options) {
    const generatorOutput = options.generator.output?.value;
    ok(generatorOutput, 'Missing generator configuration: output');
    await fs.writeFile(
      `${generatorOutput}/options-${options.generator.config.hash}.js`,
      `module.exports = ${JSON.stringify(options, undefined, 2)}`,
    );
  },
  onManifest() {
    return {
      defaultOutput: '.',
      prettyName: 'proxy generator',
    };
  },
});
Example #5
Source File: open-design-api.ts    From open-design-sdk with Apache License 2.0 6 votes vote down vote up
export async function createOpenDesignApi(
  params: { token?: string | null } = {}
): Promise<{
  openDesignApi: OpenDesignApi
  apiRoot: string
  token: string
}> {
  const apiRoot = process.env['OPEN_DESIGN_API_URL_ROOT']
  ok(apiRoot)

  const token = params.token || (await getToken({ apiRoot }))

  return {
    openDesignApi: new OpenDesignApi({
      apiRoot,
      token,
    }),
    apiRoot,
    token,
  }
}
Example #6
Source File: open-design-api.ts    From open-design-sdk with Apache License 2.0 6 votes vote down vote up
function getToken({ apiRoot }: { apiRoot: string }) {
  const email = process.env['OPEN_DESIGN_API_USER_EMAIL']
  const name = process.env['OPEN_DESIGN_API_USER_NAME']
  ok(email)
  ok(name)

  return postToken({
    apiRoot,
    email,
    name,
  })
}
Example #7
Source File: authorized-client-service.ts    From malagu with MIT License 5 votes vote down vote up
async authorize(authorizeRequest: AuthorizeRequest): Promise<AuthorizedClient | undefined> {
        const { clientRegistrationId, principal, authorizedClient, attributes } = authorizeRequest;
        const contextAttributes = await this.contextAttributesMapper.apply(authorizeRequest);

        if (attributes) {
            for (const [key, value] of attributes.entries()) {
                contextAttributes.set(key, value);
            }
        }
        const context = <AuthorizationContext>{
            principal,
            attributes: contextAttributes
        };
        if (!authorizedClient) {
            const clientRegistration = await this.clientRegistrationManager.get(clientRegistrationId);
            ok(clientRegistration, `Could not find ClientRegistration with id '${clientRegistrationId}'`);
            context.clientRegistration = clientRegistration!;
            context.authorizedClient = await this.authorizedClientManager.get(clientRegistrationId, principal.name);
        }

        let client: AuthorizedClient | undefined;

        try {
            for (const authorizedClientProvider of this.authorizedClientProviders) {
                client = await authorizedClientProvider.provide(context);
                if (client) {
                    break;
                }
            }
        } catch (error) {
            if (error instanceof OAuth2AuthorizationError) {
                await this.authorizationFailureHandler.onAuthorizationFailure(error, principal);
            }
            throw error;
        }

        if (client) {
            await this.authorizationSuccessHandler.onAuthorizationSuccess(client, principal);
            return client;
        }
        return authorizedClient;

    }
Example #8
Source File: Cache.ts    From setup-lazarus with MIT License 5 votes vote down vote up
constructor (WithCache: boolean) {
        let tempDirectory = process.env['RUNNER_TEMP'] || '';
        ok(tempDirectory, 'Expected RUNNER_TEMP to be defined');

        this._withCache = WithCache;
        this._dir = path.join(tempDirectory, 'installers');
    }
Example #9
Source File: Lazarus.ts    From setup-lazarus with MIT License 5 votes vote down vote up
private _getTempDirectory(): string {
        let tempDirectory = process.env['RUNNER_TEMP'] || '';
        ok(tempDirectory, 'Expected RUNNER_TEMP to be defined');
        tempDirectory = path.join(tempDirectory, 'installers');
        return tempDirectory;
    }
Example #10
Source File: Packages.ts    From setup-lazarus with MIT License 5 votes vote down vote up
private _getTempDirectory(): string {
        const tempDirectory = process.env['RUNNER_TEMP'] || ''
        ok(tempDirectory, 'Expected RUNNER_TEMP to be defined')
        return tempDirectory
     }
Example #11
Source File: test-generate.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export async function testGenerate(args: {
  schema: string;
  options?: string[] | string;
  provider?: 'postgresql' | 'mongodb';
  createSouceFile?: {
    text: string;
    name: string;
    type: string;
  };
  onConnect?: (emitter: AwaitEventEmitter) => void;
}) {
  const { schema, options, provider, createSouceFile, onConnect } = args;
  let project: Project | undefined;
  const connectCallback = (emitter: AwaitEventEmitter) => {
    onConnect && onConnect(emitter);
    if (createSouceFile) {
      emitter.on(
        'PostBegin',
        ({ config, project, output, getModelName }: EventArguments) => {
          const filePath = generateFileName({
            type: createSouceFile.type,
            name: createSouceFile.name,
            getModelName,
            template: config.outputFilePattern,
          });
          project.createSourceFile(`${output}/${filePath}`, createSouceFile.text, {
            overwrite: true,
          });
        },
      );
    }
    emitter.on('End', (args: { project: Project }) => {
      ({ project } = args);
    });
  };
  await generate({
    ...(await createGeneratorOptions(schema, options, provider)),
    skipAddOutputSourceFiles: true,
    connectCallback,
  });

  ok(project, 'Project is not defined');
  const sourceFiles = project.getSourceFiles();
  const emptyFieldsFiles: string[] = [];

  for (const sourceFile of sourceFiles) {
    const filePath = sourceFile.getFilePath();
    const text = sourceFile.getText();
    if (!text) {
      let message = `Project should not contain empty files: ${filePath}`;
      const fileLower = sourceFile
        .getBaseNameWithoutExtension()
        .replace(/-/g, '')
        .split('.')[0];
      const sources = sourceFiles.filter(s =>
        s
          .getClass(() => true)
          ?.getProperties()
          .find(p => String(p.getStructure().type).toLowerCase().includes(fileLower)),
      );
      if (sources.length > 0) {
        message += `, reference: ${sources.map(s => s.getBaseName()).join(', ')}`;
      }
      throw new Error(message);
    }
    const imports = sourceFile
      .getImportDeclarations()
      .map(d => d.getStructure())
      .flatMap(s => {
        return [
          ...((s.namedImports || []) as ImportSpecifierStructure[]).map(x => x.name),
          s.namespaceImport,
        ].filter(Boolean);
      });
    if (uniq(imports).length !== imports.length) {
      throw new Error(`Duplicated import in ${filePath}: ${imports.toString()}`);
    }
    // Find classes without @Field() (must define one or more fields)
    const properties = sourceFile.getClass(() => true)?.getProperties();
    if (properties && !properties.some(p => p.getDecorator('Field'))) {
      emptyFieldsFiles.push(sourceFile.getBaseName());
    }
  }
  if (emptyFieldsFiles.length > 0) {
    throw new Error(`No defined fields in ${emptyFieldsFiles.join(', ')}`);
  }

  return { project, sourceFiles };
}
Example #12
Source File: create-config.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export function createConfig(data: Record<string, unknown>) {
  const config = merge({}, unflatten(data, { delimiter: '_' })) as Record<
    string,
    unknown
  >;
  const $warnings: string[] = [];

  const configOutputFilePattern = String(
    config.outputFilePattern || `{model}/{name}.{type}.ts`,
  );

  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  let outputFilePattern = filenamify(configOutputFilePattern, {
    replacement: '/',
  })
    .replace(/\.\./g, '/')
    .replace(/\/+/g, '/');
  outputFilePattern = trim(outputFilePattern, '/');

  if (outputFilePattern !== configOutputFilePattern) {
    $warnings.push(
      `Due to invalid filepath 'outputFilePattern' changed to '${outputFilePattern}'`,
    );
  }

  if (config.reExportAll) {
    $warnings.push(`Option 'reExportAll' is deprecated, use 'reExport' instead`);
    if (toBoolean(config.reExportAll)) {
      config.reExport = 'All';
    }
  }

  const fields: Record<string, ConfigFieldSetting | undefined> = Object.fromEntries(
    Object.entries<Dictionary<string | undefined>>(
      (config.fields ?? {}) as Record<string, Dictionary<string | undefined>>,
    )
      .filter(({ 1: value }) => typeof value === 'object')
      .map(([name, value]) => {
        const fieldSetting: ConfigFieldSetting = {
          arguments: [],
          output: toBoolean(value.output),
          input: toBoolean(value.input),
          model: toBoolean(value.model),
          from: value.from,
          defaultImport: toBoolean(value.defaultImport) ? true : value.defaultImport,
          namespaceImport: value.namespaceImport,
        };
        return [name, fieldSetting];
      }),
  );

  const decorate: DecorateElement[] = [];
  const configDecorate: (Record<string, string> | undefined)[] = Object.values(
    (config.decorate as any) || {},
  );

  for (const element of configDecorate) {
    if (!element) continue;
    ok(
      element.from && element.name,
      `Missed 'from' or 'name' part in configuration for decorate`,
    );
    decorate.push({
      isMatchField: outmatch(element.field, { separator: false }),
      isMatchType: outmatch(element.type, { separator: false }),
      from: element.from,
      name: element.name,
      namedImport: toBoolean(element.namedImport),
      defaultImport: toBoolean(element.defaultImport) ? true : element.defaultImport,
      namespaceImport: element.namespaceImport,
      arguments: element.arguments ? JSON5.parse(element.arguments) : undefined,
    });
  }

  return {
    outputFilePattern,
    tsConfigFilePath: createTsConfigFilePathValue(config.tsConfigFilePath),
    combineScalarFilters: toBoolean(config.combineScalarFilters),
    noAtomicOperations: toBoolean(config.noAtomicOperations),
    reExport: (ReExport[String(config.reExport)] || ReExport.None) as ReExport,
    emitSingle: toBoolean(config.emitSingle),
    emitCompiled: toBoolean(config.emitCompiled),
    $warnings,
    fields,
    purgeOutput: toBoolean(config.purgeOutput),
    useInputType: createUseInputType(config.useInputType as any),
    noTypeId: toBoolean(config.noTypeId),
    requireSingleFieldsInWhereUniqueInput: toBoolean(
      config.requireSingleFieldsInWhereUniqueInput,
    ),
    graphqlScalars: (config.graphqlScalars || {}) as Record<
      string,
      ImportNameSpec | undefined
    >,
    decorate,
  };
}
Example #13
Source File: output-type.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export function outputType(outputType: OutputType, args: EventArguments) {
  const { getSourceFile, models, eventEmitter, fieldSettings, getModelName, config } =
    args;
  const importDeclarations = new ImportDeclarationMap();

  const fileType = 'output';
  const modelName = getModelName(outputType.name) || '';
  const model = models.get(modelName);
  const isAggregateOutput =
    model &&
    /(?:Count|Avg|Sum|Min|Max)AggregateOutputType$/.test(outputType.name) &&
    String(outputType.name).startsWith(model.name);
  const isCountOutput =
    model?.name && outputType.name === `${model.name}CountOutputType`;
  // Get rid of bogus suffixes
  outputType.name = getOutputTypeName(outputType.name);

  if (isAggregateOutput) {
    eventEmitter.emitSync('AggregateOutput', { ...args, outputType });
  }

  const sourceFile = getSourceFile({
    name: outputType.name,
    type: fileType,
  });

  const classStructure: ClassDeclarationStructure = {
    kind: StructureKind.Class,
    isExported: true,
    name: outputType.name,
    decorators: [
      {
        name: 'ObjectType',
        arguments: [],
      },
    ],
    properties: [],
  };

  importDeclarations.add('Field', nestjsGraphql);
  importDeclarations.add('ObjectType', nestjsGraphql);

  for (const field of outputType.fields) {
    const { location, isList, type } = field.outputType;
    const outputTypeName = getOutputTypeName(String(type));
    const settings = isCountOutput
      ? undefined
      : model && fieldSettings.get(model.name)?.get(field.name);
    const propertySettings = settings?.getPropertyType({
      name: outputType.name,
      output: true,
    });
    const isCustomsApplicable =
      outputTypeName === model?.fields.find(f => f.name === field.name)?.type;

    field.outputType.type = outputTypeName;

    const propertyType = castArray(
      propertySettings?.name ||
        getPropertyType({
          location,
          type: outputTypeName,
        }),
    );

    const property = propertyStructure({
      name: field.name,
      isNullable: field.isNullable,
      hasQuestionToken: isCountOutput ? true : undefined,
      propertyType,
      isList,
    });

    classStructure.properties?.push(property);

    if (propertySettings) {
      importDeclarations.create({ ...propertySettings });
    } else if (propertyType.includes('Decimal')) {
      importDeclarations.add('Decimal', '@prisma/client/runtime');
    }

    // Get graphql type
    let graphqlType: string;
    const shouldHideField =
      settings?.shouldHideField({
        name: outputType.name,
        output: true,
      }) ||
      config.decorate.some(
        d =>
          d.name === 'HideField' &&
          d.from === '@nestjs/graphql' &&
          d.isMatchField(field.name) &&
          d.isMatchType(outputTypeName),
      );

    const fieldType = settings?.getFieldType({
      name: outputType.name,
      output: true,
    });

    if (fieldType && isCustomsApplicable && !shouldHideField) {
      graphqlType = fieldType.name;
      importDeclarations.create({ ...fieldType });
    } else {
      const graphqlImport = getGraphqlImport({
        config,
        sourceFile,
        fileType,
        location,
        isId: false,
        typeName: outputTypeName,
        getSourceFile,
      });

      graphqlType = graphqlImport.name;
      let referenceName = propertyType[0];
      if (location === 'enumTypes') {
        referenceName = last(referenceName.split(' ')) as string;
      }

      if (
        graphqlImport.specifier &&
        !importDeclarations.has(graphqlImport.name) &&
        ((graphqlImport.name !== outputType.name && !shouldHideField) ||
          (shouldHideField && referenceName === graphqlImport.name))
      ) {
        importDeclarations.set(graphqlImport.name, {
          namedImports: [{ name: graphqlImport.name }],
          moduleSpecifier: graphqlImport.specifier,
        });
      }
    }

    ok(property.decorators, 'property.decorators is undefined');

    if (shouldHideField) {
      importDeclarations.add('HideField', nestjsGraphql);
      property.decorators.push({ name: 'HideField', arguments: [] });
    } else {
      // Generate `@Field()` decorator
      property.decorators.push({
        name: 'Field',
        arguments: [
          isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`,
          JSON5.stringify({
            ...settings?.fieldArguments(),
            nullable: Boolean(field.isNullable),
          }),
        ],
      });

      if (isCustomsApplicable) {
        for (const options of settings || []) {
          if (
            (options.kind === 'Decorator' &&
              options.output &&
              options.match?.(field.name)) ??
            true
          ) {
            property.decorators.push({
              name: options.name,
              arguments: options.arguments as string[],
            });
            ok(options.from, "Missed 'from' part in configuration or field setting");
            importDeclarations.create(options);
          }
        }
      }
    }

    eventEmitter.emitSync('ClassProperty', property, {
      location,
      isList,
      propertyType,
    });
  }

  sourceFile.set({
    statements: [...importDeclarations.toStatements(), classStructure],
  });
}
Example #14
Source File: model-output-type.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export function modelOutputType(outputType: OutputType, args: EventArguments) {
  const { getSourceFile, models, config, modelFields, fieldSettings, eventEmitter } =
    args;
  const model = models.get(outputType.name);
  ok(model, `Cannot find model by name ${outputType.name}`);

  const sourceFile = getSourceFile({
    name: outputType.name,
    type: 'model',
  });
  const sourceFileStructure = sourceFile.getStructure();
  const exportDeclaration = getExportDeclaration(
    model.name,
    sourceFileStructure.statements as StatementStructures[],
  );
  const importDeclarations = new ImportDeclarationMap();
  const classStructure: ClassDeclarationStructure = {
    kind: StructureKind.Class,
    isExported: true,
    name: outputType.name,
    decorators: [
      {
        name: 'ObjectType',
        arguments: [],
      },
    ],
    properties: [],
  };
  (sourceFileStructure.statements as StatementStructures[]).push(classStructure);
  ok(classStructure.decorators, 'classStructure.decorators is undefined');
  const decorator = classStructure.decorators.find(d => d.name === 'ObjectType');
  ok(decorator, 'ObjectType decorator not found');

  let modelSettings: ObjectSettings | undefined;
  // Get model settings from documentation
  if (model.documentation) {
    const objectTypeOptions: PlainObject = {};
    const { documentation, settings } = createObjectSettings({
      text: model.documentation,
      config,
    });
    if (documentation) {
      if (!classStructure.leadingTrivia) {
        classStructure.leadingTrivia = createComment(documentation);
      }
      objectTypeOptions.description = documentation;
    }
    decorator.arguments = settings.getObjectTypeArguments(objectTypeOptions);
    modelSettings = settings;
  }

  importDeclarations.add('Field', nestjsGraphql);
  importDeclarations.add('ObjectType', nestjsGraphql);

  for (const field of outputType.fields) {
    let fileType = 'model';
    const { location, isList, type, namespace } = field.outputType;

    let outputTypeName = String(type);
    if (namespace !== 'model') {
      fileType = 'output';
      outputTypeName = getOutputTypeName(outputTypeName);
    }
    const modelField = modelFields.get(model.name)?.get(field.name);
    const settings = fieldSettings.get(model.name)?.get(field.name);
    const fieldType = settings?.getFieldType({
      name: outputType.name,
      output: true,
    });
    const propertySettings = settings?.getPropertyType({
      name: outputType.name,
      output: true,
    });

    const propertyType = castArray(
      propertySettings?.name ||
        getPropertyType({
          location,
          type: outputTypeName,
        }),
    );

    // For model we keep only one type
    propertyType.splice(1, propertyType.length);

    if (field.isNullable && !isList) {
      propertyType.push('null');
    }

    let graphqlType: string;

    if (fieldType) {
      graphqlType = fieldType.name;
      importDeclarations.create({ ...fieldType });
    } else {
      const graphqlImport = getGraphqlImport({
        config,
        sourceFile,
        fileType,
        location,
        isId: modelField?.isId,
        noTypeId: config.noTypeId,
        typeName: outputTypeName,
        getSourceFile,
      });

      graphqlType = graphqlImport.name;

      if (graphqlImport.name !== outputType.name && graphqlImport.specifier) {
        importDeclarations.add(graphqlImport.name, graphqlImport.specifier);
      }
    }

    const property = propertyStructure({
      name: field.name,
      isNullable: field.isNullable,
      hasExclamationToken: true,
      hasQuestionToken: location === 'outputObjectTypes',
      propertyType,
      isList,
    });

    if (typeof property.leadingTrivia === 'string' && modelField?.documentation) {
      property.leadingTrivia += createComment(modelField.documentation, settings);
    }

    classStructure.properties?.push(property);

    if (propertySettings) {
      importDeclarations.create({ ...propertySettings });
    } else if (propertyType.includes('Decimal')) {
      importDeclarations.add('Decimal', '@prisma/client/runtime');
    }

    ok(property.decorators, 'property.decorators is undefined');

    if (settings?.shouldHideField({ name: outputType.name, output: true })) {
      importDeclarations.add('HideField', nestjsGraphql);
      property.decorators.push({ name: 'HideField', arguments: [] });
    } else {
      // Generate `@Field()` decorator
      property.decorators.push({
        name: 'Field',
        arguments: [
          isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`,
          JSON5.stringify({
            ...settings?.fieldArguments(),
            nullable: Boolean(field.isNullable),
            defaultValue: ['number', 'string', 'boolean'].includes(
              typeof modelField?.default,
            )
              ? modelField?.default
              : undefined,
            description: modelField?.documentation,
          }),
        ],
      });

      for (const setting of settings || []) {
        if (shouldBeDecorated(setting) && (setting.match?.(field.name) ?? true)) {
          property.decorators.push({
            name: setting.name,
            arguments: setting.arguments as string[],
          });
          ok(setting.from, "Missed 'from' part in configuration or field setting");
          importDeclarations.create(setting);
        }
      }

      for (const decorate of config.decorate) {
        if (decorate.isMatchField(field.name) && decorate.isMatchType(outputTypeName)) {
          property.decorators.push({
            name: decorate.name,
            arguments: decorate.arguments?.map(x => pupa(x, { propertyType })),
          });
          importDeclarations.create(decorate);
        }
      }
    }

    eventEmitter.emitSync('ClassProperty', property, {
      location,
      isList,
      propertyType,
    });
  }

  // Generate class decorators from model settings
  for (const setting of modelSettings || []) {
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
    if (shouldBeDecorated(setting)) {
      classStructure.decorators.push({
        name: setting.name,
        arguments: setting.arguments as string[],
      });
      importDeclarations.create(setting);
    }
  }

  if (exportDeclaration) {
    sourceFile.set({
      statements: [exportDeclaration, '\n', classStructure],
    });
    const classDeclaration = sourceFile.getClassOrThrow(model.name);
    const commentedText = classDeclaration
      .getText()
      .split('\n')
      .map(x => `// ${x}`);
    classDeclaration.remove();
    sourceFile.addStatements(['\n', ...commentedText]);
  } else {
    sourceFile.set({
      statements: [...importDeclarations.toStatements(), classStructure],
    });
  }
}
Example #15
Source File: input-type.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export function inputType(
  args: EventArguments & {
    inputType: InputType;
    fileType: string;
    classDecoratorName: string;
  },
) {
  const {
    classDecoratorName,
    classTransformerTypeModels,
    config,
    eventEmitter,
    fieldSettings,
    fileType,
    getModelName,
    getSourceFile,
    inputType,
    models,
    removeTypes,
    typeNames,
  } = args;

  typeNames.add(inputType.name);

  const importDeclarations = new ImportDeclarationMap();
  const sourceFile = getSourceFile({
    name: inputType.name,
    type: fileType,
  });
  const classStructure: ClassDeclarationStructure = {
    kind: StructureKind.Class,
    isExported: true,
    name: inputType.name,
    decorators: [
      {
        name: classDecoratorName,
        arguments: [],
      },
    ],
    properties: [],
  };
  const modelName = getModelName(inputType.name) || '';
  const model = models.get(modelName);
  const modelFieldSettings = model && fieldSettings.get(model.name);
  const moduleSpecifier = '@nestjs/graphql';

  // console.log('sourceFile.getBaseName()', sourceFile.getBaseName());

  importDeclarations
    .set('Field', {
      namedImports: [{ name: 'Field' }],
      moduleSpecifier,
    })
    .set(classDecoratorName, {
      namedImports: [{ name: classDecoratorName }],
      moduleSpecifier,
    });

  const useInputType = config.useInputType.find(x =>
    inputType.name.includes(x.typeName),
  );

  for (const field of inputType.fields) {
    field.inputTypes = field.inputTypes.filter(t => !removeTypes.has(String(t.type)));
    eventEmitter.emitSync('BeforeGenerateField', field, args);

    const { inputTypes, isRequired, name } = field;

    if (inputTypes.length === 0) {
      // No types
      continue;
    }

    const usePattern = useInputType?.ALL || useInputType?.[name];
    const graphqlInputType = getGraphqlInputType(inputTypes, usePattern);
    const { isList, location, type } = graphqlInputType;
    const typeName = String(type);
    const settings = modelFieldSettings?.get(name);
    const propertySettings = settings?.getPropertyType({
      name: inputType.name,
      input: true,
    });
    const modelField = model?.fields.find(f => f.name === name);
    const isCustomsApplicable = typeName === modelField?.type;
    const propertyType = castArray(
      propertySettings?.name ||
        getPropertyType({
          location,
          type: typeName,
        }),
    );
    const property = propertyStructure({
      name,
      isNullable: !isRequired,
      propertyType,
      isList,
    });
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    classStructure.properties!.push(property);

    if (propertySettings) {
      importDeclarations.create({ ...propertySettings });
    } else if (propertyType.includes('Decimal')) {
      importDeclarations.add('Decimal', '@prisma/client/runtime');
    }

    // Get graphql type
    let graphqlType: string;
    const shouldHideField =
      settings?.shouldHideField({
        name: inputType.name,
        input: true,
      }) ||
      config.decorate.some(
        d =>
          d.name === 'HideField' &&
          d.from === '@nestjs/graphql' &&
          d.isMatchField(name) &&
          d.isMatchType(inputType.name),
      );

    const fieldType = settings?.getFieldType({
      name: inputType.name,
      input: true,
    });

    if (fieldType && isCustomsApplicable && !shouldHideField) {
      graphqlType = fieldType.name;
      importDeclarations.create({ ...fieldType });
    } else {
      // Import property type class
      const graphqlImport = getGraphqlImport({
        config,
        sourceFile,
        location,
        typeName,
        getSourceFile,
      });

      graphqlType = graphqlImport.name;
      let referenceName = propertyType[0];
      if (location === 'enumTypes') {
        referenceName = last(referenceName.split(' ')) as string;
      }

      if (
        graphqlImport.specifier &&
        !importDeclarations.has(graphqlImport.name) &&
        graphqlImport.name !== inputType.name
        // ((graphqlImport.name !== inputType.name && !shouldHideField) ||
        //     (shouldHideField && referenceName === graphqlImport.name))
      ) {
        importDeclarations.set(graphqlImport.name, {
          namedImports: [{ name: graphqlImport.name }],
          moduleSpecifier: graphqlImport.specifier,
        });
      }
    }

    ok(property.decorators, 'property.decorators is undefined');

    if (shouldHideField) {
      importDeclarations.add('HideField', '@nestjs/graphql');
      property.decorators.push({ name: 'HideField', arguments: [] });
    } else {
      // Generate `@Field()` decorator
      property.decorators.push({
        name: 'Field',
        arguments: [
          isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`,
          JSON5.stringify({
            ...settings?.fieldArguments(),
            nullable: !isRequired,
          }),
        ],
      });

      // Debug
      // if (classStructure.name === 'XCreateInput') {
      //   console.log({
      //     field,
      //     property,
      //     modelField,
      //     graphqlInputType,
      //     'args.inputType': args.inputType,
      //     'classStructure.name': classStructure.name,
      //     classTransformerTypeModels,
      //     modelName,
      //     graphqlType,
      //   });
      // }

      if (graphqlType === 'GraphQLDecimal') {
        importDeclarations.add('transformToDecimal', 'prisma-graphql-type-decimal');
        importDeclarations.add('Transform', 'class-transformer');
        importDeclarations.add('Type', 'class-transformer');

        property.decorators.push(
          {
            name: 'Type',
            arguments: ['() => Object'],
          },
          {
            name: 'Transform',
            arguments: ['transformToDecimal'],
          },
        );
      } else if (
        location === 'inputObjectTypes' &&
        (modelField?.type === 'Decimal' ||
          [
            'connect',
            'connectOrCreate',
            'create',
            'createMany',
            'data',
            'delete',
            'deleteMany',
            'disconnect',
            'set',
            'update',
            'updateMany',
            'upsert',
            'where',
          ].includes(name) ||
          classTransformerTypeModels.has(getModelName(graphqlType) || ''))
      ) {
        importDeclarations.add('Type', 'class-transformer');
        property.decorators.push({ name: 'Type', arguments: [`() => ${graphqlType}`] });
      }

      if (isCustomsApplicable) {
        for (const options of settings || []) {
          if (
            (options.kind === 'Decorator' && options.input && options.match?.(name)) ??
            true
          ) {
            property.decorators.push({
              name: options.name,
              arguments: options.arguments as string[],
            });
            ok(options.from, "Missed 'from' part in configuration or field setting");
            importDeclarations.create(options);
          }
        }
      }

      for (const decorate of config.decorate) {
        if (decorate.isMatchField(name) && decorate.isMatchType(inputType.name)) {
          property.decorators.push({
            name: decorate.name,
            arguments: decorate.arguments?.map(x => pupa(x, { propertyType })),
          });
          importDeclarations.create(decorate);
        }
      }
    }

    eventEmitter.emitSync('ClassProperty', property, {
      location,
      isList,
      propertyType,
    });
  }

  sourceFile.set({
    statements: [...importDeclarations.toStatements(), classStructure],
  });
}
Example #16
Source File: generate-files.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export async function generateFiles(args: EventArguments) {
  const { project, config, output, eventEmitter } = args;

  if (config.emitSingle) {
    const rootDirectory =
      project.getDirectory(output) || project.createDirectory(output);
    const sourceFile =
      rootDirectory.getSourceFile('index.ts') ||
      rootDirectory.createSourceFile('index.ts', undefined, { overwrite: true });
    const statements = project.getSourceFiles().flatMap(s => {
      if (s === sourceFile) {
        return [];
      }
      const classDeclaration = s.getClass(() => true);
      const statements = s.getStructure().statements;
      // Reget decorator full name
      // TODO: Check possible bug of ts-morph
      if (Array.isArray(statements)) {
        for (const statement of statements) {
          if (
            !(typeof statement === 'object' && statement.kind === StructureKind.Class)
          ) {
            continue;
          }
          for (const property of statement.properties || []) {
            for (const decorator of property.decorators || []) {
              const fullName = classDeclaration
                ?.getProperty(property.name)
                ?.getDecorator(decorator.name)
                ?.getFullName();
              ok(
                fullName,
                `Cannot get full name of decorator of class ${statement.name!}`,
              );
              decorator.name = fullName;
            }
          }
        }
      }

      project.removeSourceFile(s);
      return statements;
    });
    const imports = new ImportDeclarationMap();
    const enums: (StatementStructures | string)[] = [];
    const classes: ClassDeclarationStructure[] = [];
    for (const statement of statements as (StatementStructures | string)[]) {
      if (typeof statement === 'string') {
        if (statement.startsWith('registerEnumType')) {
          enums.push(statement);
        }
        continue;
      }
      switch (statement.kind) {
        case StructureKind.ImportDeclaration:
          if (
            statement.moduleSpecifier.startsWith('./') ||
            statement.moduleSpecifier.startsWith('..')
          ) {
            continue;
          }
          for (const namedImport of statement.namedImports as ImportSpecifierStructure[]) {
            const name = namedImport.alias || namedImport.name;
            imports.add(name, statement.moduleSpecifier);
          }
          if (statement.defaultImport) {
            imports.create({
              from: statement.moduleSpecifier,
              name: statement.defaultImport,
              defaultImport: statement.defaultImport,
            });
          }
          if (statement.namespaceImport) {
            imports.create({
              from: statement.moduleSpecifier,
              name: statement.namespaceImport,
              namespaceImport: statement.namespaceImport,
            });
          }
          break;
        case StructureKind.Enum:
          enums.unshift(statement);
          break;
        case StructureKind.Class:
          classes.push(statement);
          break;
      }
    }
    sourceFile.set({
      kind: StructureKind.SourceFile,
      statements: [...imports.toStatements(), ...enums, ...classes],
    });
  }

  if (config.emitCompiled) {
    project.compilerOptions.set({
      declaration: true,
      declarationDir: output,
      rootDir: output,
      outDir: output,
      emitDecoratorMetadata: false,
      skipLibCheck: true,
    });
    const emitResult = await project.emit();
    const errors = emitResult.getDiagnostics().map(d => String(d.getMessageText()));
    if (errors.length > 0) {
      eventEmitter.emitSync('Warning', errors);
    }
  } else {
    await project.save();
  }
}
Example #17
Source File: generate.ts    From prisma-nestjs-graphql with MIT License 4 votes vote down vote up
export async function generate(
  args: GeneratorOptions & {
    skipAddOutputSourceFiles?: boolean;
    connectCallback?: (
      emitter: AwaitEventEmitter,
      eventArguments: EventArguments,
    ) => void | Promise<void>;
  },
) {
  const { connectCallback, generator, skipAddOutputSourceFiles, dmmf } = args;

  const generatorOutputValue = generator.output?.value;
  ok(generatorOutputValue, 'Missing generator configuration: output');

  const eventEmitter = new AwaitEventEmitter();
  eventEmitter.on('Warning', warning);
  eventEmitter.on('Model', modelData);
  eventEmitter.on('EnumType', registerEnum);
  eventEmitter.on('OutputType', outputType);
  eventEmitter.on('ModelOutputType', modelOutputType);
  eventEmitter.on('AggregateOutput', createAggregateInput);
  eventEmitter.on('InputType', inputType);
  eventEmitter.on('ArgsType', argsType);
  eventEmitter.on('GenerateFiles', generateFiles);

  const config = createConfig(generator.config);
  for (const message of config.$warnings) {
    eventEmitter.emitSync('Warning', message);
  }

  const project = new Project({
    tsConfigFilePath: config.tsConfigFilePath,
    skipAddingFilesFromTsConfig: true,
    skipLoadingLibFiles: !config.emitCompiled,
    manipulationSettings: {
      quoteKind: QuoteKind.Single,
    },
  });

  if (!skipAddOutputSourceFiles) {
    project.addSourceFilesAtPaths([
      `${generatorOutputValue}/**/*.ts`,
      `!${generatorOutputValue}/**/*.d.ts`,
    ]);
  }

  config.combineScalarFilters && combineScalarFilters(eventEmitter);
  config.noAtomicOperations && noAtomicOperations(eventEmitter);
  config.reExport !== ReExport.None && reExport(eventEmitter);
  config.emitSingle && emitSingle(eventEmitter);
  config.purgeOutput && purgeOutput(eventEmitter);
  config.requireSingleFieldsInWhereUniqueInput &&
    requireSingleFieldsInWhereUniqueInput(eventEmitter);

  const models = new Map<string, Model>();
  const modelNames: string[] = [];
  const modelFields = new Map<string, Map<string, Field>>();
  const fieldSettings = new Map<string, Map<string, ObjectSettings>>();
  const getModelName = createGetModelName(modelNames);
  const getSourceFile = factoryGetSourceFile({
    output: generatorOutputValue,
    project,
    getModelName,
    outputFilePattern: config.outputFilePattern,
    eventEmitter,
  });
  const {
    datamodel,
    schema: { inputObjectTypes, outputObjectTypes, enumTypes },
  } = JSON.parse(JSON.stringify(dmmf)) as DMMF.Document;
  const removeTypes = new Set<string>();
  const eventArguments: EventArguments = {
    models,
    config,
    modelNames,
    modelFields,
    fieldSettings,
    project,
    output: generatorOutputValue,
    getSourceFile,
    eventEmitter,
    typeNames: new Set<string>(),
    enums: mapKeys(datamodel.enums, x => x.name),
    getModelName,
    removeTypes,
    classTransformerTypeModels: new Set(),
  };

  if (connectCallback) {
    await connectCallback(eventEmitter, eventArguments);
  }

  await eventEmitter.emit('Begin', eventArguments);

  for (const model of datamodel.models) {
    await eventEmitter.emit('Model', model, eventArguments);
  }

  // Types behaves like model
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
  for (const model of datamodel.types || []) {
    await eventEmitter.emit('Model', model, eventArguments);
  }

  await eventEmitter.emit('PostBegin', eventArguments);

  for (const enumType of enumTypes.prisma.concat(enumTypes.model || [])) {
    await eventEmitter.emit('EnumType', enumType, eventArguments);
  }

  for (const outputType of outputObjectTypes.model) {
    await eventEmitter.emit('ModelOutputType', outputType, eventArguments);
  }

  const queryOutputTypes: OutputType[] = [];

  for (const outputType of outputObjectTypes.prisma) {
    if (['Query', 'Mutation'].includes(outputType.name)) {
      queryOutputTypes.push(outputType);
      continue;
    }
    await eventEmitter.emit('OutputType', outputType, eventArguments);
  }

  const inputTypes = inputObjectTypes.prisma.concat(inputObjectTypes.model || []);

  for (const inputType of inputTypes) {
    const event = {
      ...eventArguments,
      inputType,
      fileType: 'input',
      classDecoratorName: 'InputType',
    };
    if (inputType.fields.length === 0) {
      removeTypes.add(inputType.name);
      continue;
    }
    await eventEmitter.emit('BeforeInputType', event);
    await eventEmitter.emit('InputType', event);
  }

  for (const outputType of queryOutputTypes) {
    for (const field of outputType.fields) {
      await eventEmitter.emit('ArgsType', field, eventArguments);
    }
  }

  await eventEmitter.emit('BeforeGenerateFiles', eventArguments);
  await eventEmitter.emit('GenerateFiles', eventArguments);
  await eventEmitter.emit('End', eventArguments);

  for (const name of Object.keys(eventEmitter._events)) {
    eventEmitter.off(name);
  }
}
Example #18
Source File: sdk.spec.ts    From open-design-sdk with Apache License 2.0 4 votes vote down vote up
describe('DesignFacade', () => {
  describe('local files', () => {
    it('should return the manifest read from disk', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const { octopusFilename, manifest } = await createOctopusFile(
        'file.octopus'
      )
      const designFacade = await sdk.openOctopusFile(octopusFilename)

      deepStrictEqual(designFacade.getManifest(), manifest)
    })

    it('should return artboard octopus data read from disk', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const { octopusFilename, artboardOctopuses } = await createOctopusFile(
        'file.octopus'
      )
      const designFacade = await sdk.openOctopusFile(octopusFilename)

      const artboardFacade = designFacade.getArtboardById('a')
      ok(artboardFacade)
      deepStrictEqual(await artboardFacade.getContent(), artboardOctopuses['a'])
    })

    it('should save its manifest to a new location', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const {
        octopusFilename: originalFilename,
        manifest,
      } = await createOctopusFile('original.octopus')
      const designFacade = await sdk.openOctopusFile(originalFilename)

      const copyFilename = await createTempFileTarget('copy.octopus')
      await designFacade.saveOctopusFile({ filePath: copyFilename })

      deepStrictEqual(
        JSON.parse(readFileSync(`${copyFilename}/manifest.json`, 'utf8')),
        manifest
      )
    })

    it('should save its artboard octopus data to a new location', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const {
        octopusFilename: originalFilename,
        artboardOctopuses,
      } = await createOctopusFile('original.octopus')
      const designFacade = await sdk.openOctopusFile(originalFilename)

      const copyFilename = await createTempFileTarget('copy.octopus')
      await designFacade.saveOctopusFile({ filePath: copyFilename })

      deepStrictEqual(
        JSON.parse(
          readFileSync(`${copyFilename}/artboards/a/data.json`, 'utf8')
        ),
        artboardOctopuses['a']
      )
    })

    it('should save its bitmaps referenced via relative paths in octopus data to a new location', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const {
        octopusFilename: originalFilename,
        bitmapFilenames,
        bitmapMapping,
      } = await createOctopusFile('original.octopus')

      const designFacade = await sdk.openOctopusFile(originalFilename)

      const copyFilename = await createTempFileTarget('copy.octopus')
      await designFacade.saveOctopusFile({ filePath: copyFilename })

      deepStrictEqual(
        JSON.parse(readFileSync(`${copyFilename}/bitmaps.json`, 'utf8')),
        bitmapMapping
      )

      Object.entries(bitmapFilenames).forEach(
        ([_bitmapKey, bitmapFilename]) => {
          statSync(bitmapFilename)
        }
      )
    })

    it('should fail opening a non-existent octopus file', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const randomFilename = await createTempFileTarget('random-file.octopus')
      const [result] = await Promise.allSettled([
        sdk.openOctopusFile(randomFilename),
      ])
      strictEqual(result.status, 'rejected')
    })

    it('should fail opening an octopus file without the .octopus extension', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const { octopusFilename } = await createOctopusFile('file.random')
      const [result] = await Promise.allSettled([
        sdk.openOctopusFile(octopusFilename),
      ])
      strictEqual(result.status, 'rejected')
    })

    it('should fail opening an octopus file without a manifest', async () => {
      const { sdk } = await createSdk({ localDesigns: true })

      const { octopusFilename } = await createOctopusFile('file.octopus')
      unlinkSync(`${octopusFilename}/manifest.json`)

      const [result] = await Promise.allSettled([
        sdk.openOctopusFile(octopusFilename),
      ])
      strictEqual(result.status, 'rejected')
    })
  })

  describe('design uploads', () => {
    it('should return contents of an uploaded design', async function () {
      const { sdk } = await createSdk({ designFiles: true, api: true })

      const designFacade = await sdk.importDesignFile(
        singleArtboardSketchFileFixture.filename
      )

      const [fixtureArtboardDesc] = singleArtboardSketchFileFixture.artboards
      ok(fixtureArtboardDesc)

      const artboardFacade = designFacade.findArtboard({
        name: fixtureArtboardDesc.name,
      })
      ok(artboardFacade)

      const octopus = await artboardFacade.getContent()
      strictEqual(octopus['bounds']?.['width'], fixtureArtboardDesc.width)
      strictEqual(octopus['bounds']?.['height'], fixtureArtboardDesc.height)
    })

    it('should list all bitmaps in an uploaded design', async function () {
      const { sdk } = await createSdk({ designFiles: true, api: true })

      const designFacade = await sdk.importDesignFile(
        multiArtboardSketchFileFixture.filename
      )

      const bitmapAssetDesc = await designFacade.getBitmapAssets()
      strictEqual(
        bitmapAssetDesc.length,
        multiArtboardSketchFileFixture.bitmapCount
      )
    })
  })

  describe('design downloading', () => {
    let designId: string
    let tokenFromBefore: string

    before(async function () {
      const { sdk, token } = await createSdk({
        designFiles: true,
        api: true,
      })
      ok(token)
      tokenFromBefore = token

      const designFacade = await sdk.importDesignFile(
        singleArtboardSketchFileFixture.filename
      )
      ok(designFacade.id)
      designId = designFacade.id
    })

    it('should return contents of an uploaded design', async function () {
      const { sdk } = await createSdk({ token: tokenFromBefore, api: true })
      const designFacade = await sdk.fetchDesignById(designId)

      const [fixtureArtboardDesc] = singleArtboardSketchFileFixture.artboards
      ok(fixtureArtboardDesc)

      const artboardFacade = designFacade.findArtboard({
        name: fixtureArtboardDesc.name,
      })
      ok(artboardFacade)

      const octopus = await artboardFacade.getContent()
      strictEqual(octopus['bounds']?.['width'], fixtureArtboardDesc.width)
      strictEqual(octopus['bounds']?.['height'], fixtureArtboardDesc.height)
    })

    it('should save contents of an uploaded design', async function () {
      const { sdk } = await createSdk({
        token: tokenFromBefore,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.fetchDesignById(designId)

      const filename = await createTempFileTarget('file.octopus')
      await designFacade.saveOctopusFile({ filePath: filename })

      const manifest: ManifestData = JSON.parse(
        readFileSync(`${filename}/manifest.json`, 'utf8')
      )
      strictEqual(
        manifest['artboards'].length,
        singleArtboardSketchFileFixture.artboards.length
      )

      const [fixtureArtboardDesc] = singleArtboardSketchFileFixture.artboards
      ok(fixtureArtboardDesc)

      const octopus: OctopusDocument = JSON.parse(
        readFileSync(
          `${filename}/artboards/${fixtureArtboardDesc.id}/data.json`,
          'utf8'
        )
      )
      strictEqual(octopus['bounds']?.['width'], fixtureArtboardDesc.width)
      strictEqual(octopus['bounds']?.['height'], fixtureArtboardDesc.height)
    })

    it('should save info about the API design', async function () {
      const { sdk, apiRoot } = await createSdk({
        token: tokenFromBefore,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.fetchDesignById(designId)

      const filename = await createTempFileTarget('file.octopus')
      await designFacade.saveOctopusFile({ filePath: filename })

      const apiDesignInfo: ApiDesignInfo = JSON.parse(
        readFileSync(`${filename}/api-design.json`, 'utf8')
      )
      deepStrictEqual(apiDesignInfo, { apiRoot, designId })
    })

    it('should cache the manifest of a fetched design in a local file', async function () {
      const { sdk } = await createSdk({
        token: tokenFromBefore,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.fetchDesignById(designId)

      const filename = designFacade.octopusFilename
      ok(filename)

      const manifest: ManifestData = JSON.parse(
        readFileSync(`${filename}/manifest.json`, 'utf8')
      )
      strictEqual(
        manifest['artboards'].length,
        singleArtboardSketchFileFixture.artboards.length
      )
    })

    it('should cache artboard octopuses of a fetched design in a local file', async function () {
      const { sdk } = await createSdk({
        token: tokenFromBefore,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.fetchDesignById(designId)

      const filename = designFacade.octopusFilename
      ok(filename)

      const [fixtureArtboardDesc] = singleArtboardSketchFileFixture.artboards
      ok(fixtureArtboardDesc)

      const artboardFacade = designFacade.getArtboardById(
        fixtureArtboardDesc.id
      )
      const artboardOctopus = await artboardFacade?.getContent()
      ok(artboardOctopus)

      const localOctopus: OctopusDocument = JSON.parse(
        readFileSync(
          `${filename}/artboards/${fixtureArtboardDesc.id}/data.json`,
          'utf8'
        )
      )
      deepStrictEqual(localOctopus, artboardOctopus)
    })

    it('should continue using the API design reference after reopening the cache', async function () {
      const { sdk } = await createSdk({
        token: tokenFromBefore,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.fetchDesignById(designId)
      const filename = designFacade.octopusFilename
      ok(filename)

      const reopenedDesignFacade = await sdk.openOctopusFile(filename)

      const [fixtureArtboardDesc] = singleArtboardSketchFileFixture.artboards
      ok(fixtureArtboardDesc)

      const artboardFacade = reopenedDesignFacade.getArtboardById(
        fixtureArtboardDesc.id
      )
      const artboardOctopus = await artboardFacade?.getContent()
      ok(artboardOctopus)
    })
  })

  describe('asset downloading', () => {
    it('should download all bitmaps from an uploaded design', async function () {
      const { sdk } = await createSdk({
        designFiles: true,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.importDesignFile(
        multiArtboardSketchFileFixture.filename
      )

      const filename = designFacade.octopusFilename
      ok(filename)

      const bitmapAssetDescs = await designFacade.getBitmapAssets()
      await designFacade.downloadBitmapAssets(bitmapAssetDescs)

      doesNotThrow(() => {
        const bitmapBasenames = readdirSync(`${filename}/bitmaps`)
        strictEqual(
          bitmapBasenames.length,
          multiArtboardSketchFileFixture.bitmapCount
        )
      })
    })

    it('should download prerendered bitmaps from an uploaded design to a subdirectory', async function () {
      const { sdk } = await createSdk({
        designFiles: true,
        localDesigns: true,
        api: true,
      })

      const designFacade = await sdk.importDesignFile(
        singleInlineArtboardPhotoshopFileFixture.filename
      )

      const filename = designFacade.octopusFilename
      ok(filename)

      const bitmapAssetDescs = await designFacade.getBitmapAssets()
      await designFacade.downloadBitmapAssets(bitmapAssetDescs)

      doesNotThrow(() => {
        const bitmapBasenames = readdirSync(`${filename}/bitmaps/prerendered`)
        strictEqual(
          bitmapBasenames.length,
          singleInlineArtboardPhotoshopFileFixture.prerenderedBitmapCount
        )
      })
    })
  })

  describe('figma designs', () => {
    let designId: string
    let tokenFromBefore: string

    before(async function () {
      const { sdk, token } = await createSdk({ api: true })
      ok(token)
      tokenFromBefore = token

      const designFacade = await sdk.importFigmaDesign({
        figmaToken: process.env['E2E_FIGMA_TOKEN'] || null,
        figmaFileKey: process.env['E2E_FIGMA_FILE_KEY'] || '',
      })
      ok(designFacade.id)
      designId = designFacade.id
    })

    it('should save a Sketch export result file', async function () {
      const { sdk } = await createSdk({
        token: tokenFromBefore,
        designFiles: true,
        api: true,
      })

      const designFacade = await sdk.fetchDesignById(designId)

      const filename = await createTempFileTarget('file.sketch')
      await designFacade.exportDesignFile(filename)

      doesNotThrow(() => {
        const stats = statSync(filename)
        ok(stats.isFile())
        ok(stats.size > 0)
      })
    })
  })

  describe('figma designs with immediate conversion', () => {
    it('should save a Sketch conversion result file', async function () {
      const { sdk } = await createSdk({ designFiles: true, api: true })

      const designFacade = await sdk.convertFigmaDesign({
        figmaToken: process.env['E2E_FIGMA_TOKEN'] || null,
        figmaFileKey: process.env['E2E_FIGMA_FILE_KEY'] || '',
        exports: [{ format: 'sketch' }],
      })

      const filename = await createTempFileTarget('file.sketch')
      await designFacade.exportDesignFile(filename)

      doesNotThrow(() => {
        const stats = statSync(filename)
        ok(stats.isFile())
        ok(stats.size > 0)
      })
    })
  })
})
Example #19
Source File: layer.spec.ts    From open-design-sdk with Apache License 2.0 4 votes vote down vote up
describe('Layer', () => {
  function createOctopus<T extends Partial<OctopusDocument>>(
    data: T
  ): OctopusDocument & T {
    const width = Math.round(Math.random() * 400)
    const height = Math.round(Math.random() * 400)

    return {
      'frame': {
        'x': Math.round(Math.random() * 400),
        'y': Math.round(Math.random() * 400),
      },
      'bounds': {
        'left': 0,
        'top': 0,
        'right': width,
        'bottom': height,
        'width': width,
        'height': height,
      },
      'layers': [],
      ...data,
    }
  }

  function createLayerOctopus<T extends Partial<LayerOctopusData>>(
    data: T
  ): LayerOctopusData {
    const id = String(Math.round(Math.random() * 400))
    const base = {
      'id': `layer-${id}`,
      'name': `Layer ID=${id}`,
      'type': 'layer',
    }
    const type = data['type'] || base['type']

    // @ts-expect-error Hard to generalize.
    return {
      ...base,
      ...(type === 'textLayer' ? { 'text:': {} } : {}),
      ...(type === 'groupLayer' ? { 'layers:': [] } : {}),
      ...data,
    }
  }

  describe('layer info', () => {
    it('should have the ID from octopus', () => {
      const layer = new Layer(createLayerOctopus({ 'id': 'a' }))

      strictEqual(layer.id, 'a')
    })

    it('should have the provided octopus', () => {
      const octopus = createLayerOctopus({ 'id': 'a' })
      const layer = new Layer({ ...octopus })

      deepStrictEqual(layer.octopus, octopus)
    })

    it('should have the name from octopus', () => {
      const layer = new Layer(createLayerOctopus({ 'id': 'a', 'name': 'Abc' }))

      strictEqual(layer.name, 'Abc')
    })

    it('should have the type from octopus', () => {
      const layer = new Layer(
        createLayerOctopus({ 'id': 'a', 'type': 'shapeLayer' })
      )

      strictEqual(layer.type, 'shapeLayer')
    })

    it('should return the provided artboard', () => {
      const artboard = new Artboard('x', createOctopus({}))
      const layer = new Layer(createLayerOctopus({ 'id': 'a' }), { artboard })

      strictEqual(layer.getArtboard(), artboard)
    })
  })

  describe('parent layer info', () => {
    it('should not return a parent layer ID when not parent layer ID is specified', () => {
      const layerOctopus = createLayerOctopus({ 'id': 'a' })
      const artboardOctopus = createOctopus({
        'layers': [layerOctopus],
      })

      const layer = new Layer(layerOctopus, {
        parentLayerId: null,
        artboard: new Artboard('x', artboardOctopus),
      })

      strictEqual(layer.getParentLayerId(), null)
    })

    it('should not return a parent layer ID when neither a parent layer ID is specified nor an artboard instance is provided', () => {
      const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
        parentLayerId: null,
        artboard: null,
      })

      strictEqual(layer.getParentLayerId(), null)
    })

    it('should return the specified parent layer ID', () => {
      const layerOctopus = createLayerOctopus({ 'id': 'a' })
      const artboardOctopus = createOctopus({
        'layers': [
          createLayerOctopus({
            'id': 'x',
            'layers': [
              createLayerOctopus({ 'id': 'b', 'layers': [layerOctopus] }),
            ],
          }),
        ],
      })

      const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
        parentLayerId: 'b',
        artboard: new Artboard('x', artboardOctopus),
      })

      strictEqual(layer.getParentLayerId(), 'b')
    })

    it('should return the specified parent layer ID even when an artboard instance is not provided', () => {
      const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
        parentLayerId: 'b',
        artboard: null,
      })

      strictEqual(layer.getParentLayerId(), 'b')
    })
  })

  describe('parent layers', () => {
    describe('parent layer lookup', () => {
      it('should not return a parent layer when no parent layer ID is not specified', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: null,
          artboard: new Artboard('x', artboardOctopus),
        })

        strictEqual(layer.getParentLayer(), null)
      })

      it('should not return a parent layer when neither a parent layer ID is specified nor an artboard instance is provided', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: null,
          artboard: null,
        })

        strictEqual(layer.getParentLayer(), null)
      })

      it('should fail on looking up the parent layer when a parent layer ID is specified without providing the artboard instance', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: 'b',
          artboard: null,
        })

        throws(() => {
          layer.getParentLayer()
        })
      })

      it('should fail on looking up the parent layer when the parent layer is not available on the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: 'b',
          artboard: new Artboard('x', artboardOctopus),
        })

        throws(() => {
          layer.getParentLayer()
        })
      })

      it('should return the parent layer from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'layers': [
                createLayerOctopus({ 'id': 'y', 'layers': [layerOctopus] }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'y',
          artboard,
        })

        strictEqual(layer.getParentLayer(), artboard.getLayerById('y'))
      })
    })

    describe('parent layer chain lookup', () => {
      it('should not return any parent layers when no parent layer ID is not specified', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: null,
          artboard: new Artboard('x', artboardOctopus),
        })

        strictEqual(layer.getParentLayers().length, 0)
      })

      it('should not return any parent layers when neither a parent layer ID is specified nor an artboard instance is provided', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: null,
          artboard: null,
        })

        strictEqual(layer.getParentLayers().length, 0)
      })

      it('should fail on looking up parent layers when a parent layer ID is specified without providing the artboard instance', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: 'b',
          artboard: null,
        })

        throws(() => {
          layer.getParentLayers()
        })
      })

      it('should fail on looking up parent layers when the direct parent layer is not available on the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: 'b',
          artboard: new Artboard('x', artboardOctopus),
        })

        throws(() => {
          layer.getParentLayers()
        })
      })

      it('should return the parent layer chain from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'layers': [
                createLayerOctopus({ 'id': 'y', 'layers': [layerOctopus] }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'y',
          artboard,
        })

        const parentLayers = layer.getParentLayers().getLayers()
        strictEqual(parentLayers.length, 2)
        strictEqual(parentLayers[0], artboard.getLayerById('y'))
        strictEqual(parentLayers[1], artboard.getLayerById('x'))
      })
    })

    describe('parent layer ID chain lookup', () => {
      it('should not return any parent layer IDs when no parent layer ID is not specified', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: null,
          artboard: new Artboard('x', artboardOctopus),
        })

        strictEqual(layer.getParentLayerIds().length, 0)
      })

      it('should not return any parent layer IDs when neither a parent layer ID is specified nor an artboard instance is provided', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: null,
          artboard: null,
        })

        strictEqual(layer.getParentLayerIds().length, 0)
      })

      it('should fail on looking up parent layer IDs when a parent layer ID is specified without providing the artboard instance', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: 'b',
          artboard: null,
        })

        throws(() => {
          layer.getParentLayerIds()
        })
      })

      it('should fail on looking up parent layer IDs when the direct parent layer is not available on the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: 'b',
          artboard: new Artboard('x', artboardOctopus),
        })

        throws(() => {
          layer.getParentLayerIds()
        })
      })

      it('should return the parent layer ID chain from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'layers': [
                createLayerOctopus({ 'id': 'y', 'layers': [layerOctopus] }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'y',
          artboard,
        })

        const parentLayers = layer.getParentLayerIds()
        strictEqual(parentLayers.length, 2)
        strictEqual(parentLayers[0], 'y')
        strictEqual(parentLayers[1], 'x')
      })
    })

    describe('selector-based parent layer lookup', () => {
      it('should not return a parent layer when no parent layer ID is not specified', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'x', 'name': 'Layer X' }),
            createLayerOctopus({ 'id': 'y', 'name': 'Layer Y' }),
            layerOctopus,
          ],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: null,
          artboard: new Artboard('x', artboardOctopus),
        })

        strictEqual(layer.findParentLayer({ 'name': 'Layer X' }), null)
      })

      it('should not return any parent layer when neither a parent layer ID is specified nor an artboard instance is provided', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: null,
          artboard: null,
        })

        strictEqual(layer.findParentLayer({ 'name': 'Layer X' }), null)
      })

      it('should fail on looking up parent layer when a parent layer ID is specified without providing the artboard instance', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: 'b',
          artboard: null,
        })

        throws(() => {
          layer.findParentLayer({ 'name': 'Layer X' })
        })
      })

      it('should fail on looking up parent layer when the direct parent layer is not available on the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: 'b',
          artboard: new Artboard('x', artboardOctopus),
        })

        throws(() => {
          layer.findParentLayer({ 'name': 'Layer X' })
        })
      })

      it('should return the parent layer matching the selector from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'Layer X',
              'layers': [
                createLayerOctopus({
                  'id': 'y',
                  'name': 'Layer Y',
                  'layers': [layerOctopus],
                }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'y',
          artboard,
        })

        strictEqual(
          layer.findParentLayer({ 'name': 'Layer X' }),
          artboard.getLayerById('x')
        )
      })

      it('should return the deepest parent layer matching the selector from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'ABC',
              'layers': [
                createLayerOctopus({
                  'id': 'y',
                  'name': 'ABC',
                  'layers': [layerOctopus],
                }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'y',
          artboard,
        })

        strictEqual(
          layer.findParentLayer({ name: 'ABC' }),
          artboard.getLayerById('y')
        )
      })

      it('should not return a sibling layer matching the selector from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'Layer X',
              'layers': [
                layerOctopus,
                createLayerOctopus({
                  'id': 'y',
                  'name': 'Layer Y',
                }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'x',
          artboard,
        })

        strictEqual(layer.findParentLayer({ 'name': 'Layer Y' }), null)
      })

      it('should not return a parent layer sibling layer matching the selector from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'Layer X',
              'layers': [layerOctopus],
            }),
            createLayerOctopus({
              'id': 'y',
              'name': 'Layer Y',
              'layers': [layerOctopus],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'x',
          artboard,
        })

        strictEqual(layer.findParentLayer({ 'name': 'Layer Y' }), null)
      })
    })

    describe('selector-based batch parent layer lookup', () => {
      it('should not return any parent layers when no parent layer ID is not specified', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'x', 'name': 'Layer X' }),
            createLayerOctopus({ 'id': 'y', 'name': 'Layer Y' }),
            layerOctopus,
          ],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: null,
          artboard: new Artboard('x', artboardOctopus),
        })

        strictEqual(layer.findParentLayers({ 'name': 'Layer X' }).length, 0)
      })

      it('should not return any parent layer when neither a parent layer ID is specified nor an artboard instance is provided', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: null,
          artboard: null,
        })

        strictEqual(layer.findParentLayers({ 'name': 'Layer X' }).length, 0)
      })

      it('should fail on looking up parent layer when a parent layer ID is specified without providing the artboard instance', () => {
        const layer = new Layer(createLayerOctopus({ 'id': 'a' }), {
          parentLayerId: 'b',
          artboard: null,
        })

        throws(() => {
          layer.findParentLayers({ 'name': 'Layer X' })
        })
      })

      it('should fail on looking up parent layer when the direct parent layer is not available on the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          parentLayerId: 'b',
          artboard: new Artboard('x', artboardOctopus),
        })

        throws(() => {
          layer.findParentLayers({ 'name': 'Layer X' })
        })
      })

      it('should return the parent layer matching the selector from the artboard instance sorted from the deepest layer up', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'ABC',
              'layers': [
                createLayerOctopus({
                  'id': 'y',
                  'name': 'XYZ',
                  'layers': [
                    createLayerOctopus({
                      'id': 'z',
                      'name': 'ABC',
                      'layers': [layerOctopus],
                    }),
                  ],
                }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'z',
          artboard,
        })

        const parentLayers = layer.findParentLayers({ name: 'ABC' }).getLayers()
        strictEqual(parentLayers.length, 2)
        strictEqual(parentLayers[0], artboard.getLayerById('z'))
        strictEqual(parentLayers[1], artboard.getLayerById('x'))
      })

      it('should not return sibling layers matching the selector from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'Layer X',
              'layers': [
                layerOctopus,
                createLayerOctopus({
                  'id': 'y',
                  'name': 'Layer Y',
                }),
              ],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'x',
          artboard,
        })

        strictEqual(layer.findParentLayers({ 'name': 'Layer Y' }).length, 0)
      })

      it('should not return parent layer sibling layers matching the selector from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({ 'id': 'a' })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'x',
              'name': 'Layer X',
              'layers': [layerOctopus],
            }),
            createLayerOctopus({
              'id': 'y',
              'name': 'Layer Y',
              'layers': [layerOctopus],
            }),
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          parentLayerId: 'x',
          artboard,
        })

        strictEqual(layer.findParentLayers({ 'name': 'Layer Y' }).length, 0)
      })
    })
  })

  describe('nested layers', () => {
    describe('nested layer list', () => {
      it('should return the first-level nested layers', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({ 'id': 'b' }),
            createLayerOctopus({ 'id': 'c' }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.getNestedLayers().getLayers()
        strictEqual(nestedLayers.length, 2)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        strictEqual(nestedLayers[0].id, 'b')
        strictEqual(nestedLayers[1].id, 'c')
      })

      it('should return the same first-level nested layer list on multiple calls', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({ 'id': 'b' }),
            createLayerOctopus({ 'id': 'c' }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers1 = layer.getNestedLayers()
        const nestedLayers2 = layer.getNestedLayers()
        strictEqual(nestedLayers1, nestedLayers2)
      })

      it('should not return deeper-level nested layers by default', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'layers': [createLayerOctopus({ 'id': 'c' })],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.getNestedLayers().getLayers()
        strictEqual(nestedLayers.length, 1)
        ok(nestedLayers[0])
        strictEqual(nestedLayers[0].id, 'b')
      })

      it('should return the specified number of levels of nested layers', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'layers': [createLayerOctopus({ 'id': 'd' })],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.getNestedLayers({ depth: 2 }).getLayers()
        strictEqual(nestedLayers.length, 2)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        strictEqual(nestedLayers[0].id, 'b')
        strictEqual(nestedLayers[1].id, 'c')
      })

      it('should return the same multi-level nested layer list on multiple calls', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'layers': [createLayerOctopus({ 'id': 'c' })],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers1 = layer.getNestedLayers({ depth: 2 })
        const nestedLayers2 = layer.getNestedLayers({ depth: 2 })
        strictEqual(nestedLayers1, nestedLayers2)
      })
    })

    describe('nested layer lookup', () => {
      it('should look up a first-level nested layer by a selector', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
            createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
          ],
        })

        const layer = new Layer(layerOctopus)

        strictEqual(layer.findNestedLayer({ name: 'Layer C' })?.id, 'c')
      })

      it('should look up a deeper-level nested layer by a selector', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                createLayerOctopus({ 'id': 'e', 'name': 'Layer E' }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        strictEqual(layer.findNestedLayer({ name: 'Layer C' })?.id, 'c')
      })

      it('should return a matching nested layer from any levels by default', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'name': 'Layer A',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'name': 'Layer C',
                  'layers': [
                    createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                  ],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        strictEqual(layer.findNestedLayer({ name: 'Layer D' })?.id, 'd')
      })

      it('should return a matching highest-level nested layer', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'name': 'Layer A',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'ABC',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'name': 'ABC',
                  'layers': [createLayerOctopus({ 'id': 'd', 'name': 'ABC' })],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        strictEqual(layer.findNestedLayer({ name: 'ABC' })?.id, 'b')
      })

      it('should return a matching deeper-level nested layer listed in a layer which precedes a matching sibling layer', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'name': 'Layer A',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'name': 'ABC',
                }),
              ],
            }),
            createLayerOctopus({ 'id': 'd', 'name': 'ABC' }),
          ],
        })

        const layer = new Layer(layerOctopus)

        strictEqual(layer.findNestedLayer({ name: 'ABC' })?.id, 'c')
      })

      it('should not return matching nested layers from levels deeper than the specified depth', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'name': 'Layer A',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'name': 'Layer C',
                  'layers': [createLayerOctopus({ 'id': 'd', 'name': 'ABC' })],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        strictEqual(layer.findNestedLayer({ name: 'ABC' }, { depth: 2 }), null)
      })
    })

    describe('batch nested layer lookup', () => {
      it('should look up first-level nested layers by a selector', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({ 'id': 'b', 'name': 'ABC' }),
            createLayerOctopus({ 'id': 'c', 'name': 'ABC' }),
            createLayerOctopus({ 'id': 'd', 'name': 'XYZ' }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.findNestedLayers({ name: 'ABC' }).getLayers()
        strictEqual(nestedLayers.length, 2)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        strictEqual(nestedLayers[0].id, 'b')
        strictEqual(nestedLayers[1].id, 'c')
      })

      it('should look up deeper-level nested layers by a selector', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'ABC' }),
                createLayerOctopus({ 'id': 'd', 'name': 'ABC' }),
                createLayerOctopus({ 'id': 'e', 'name': 'Layer E' }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.findNestedLayers({ name: 'ABC' }).getLayers()
        strictEqual(nestedLayers.length, 2)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        strictEqual(nestedLayers[0].id, 'c')
        strictEqual(nestedLayers[1].id, 'd')
      })

      it('should return matching nested layers from all levels by default', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({ 'id': 'b1', 'name': 'ABC' }),
            createLayerOctopus({
              'id': 'b2',
              'layers': [
                createLayerOctopus({ 'id': 'c1', 'name': 'ABC' }),
                createLayerOctopus({
                  'id': 'c2',
                  'layers': [createLayerOctopus({ 'id': 'd1', 'name': 'ABC' })],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.findNestedLayers({ name: 'ABC' }).getLayers()
        strictEqual(nestedLayers.length, 3)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        ok(nestedLayers[2])
        strictEqual(nestedLayers[0].id, 'b1')
        strictEqual(nestedLayers[1].id, 'c1')
        strictEqual(nestedLayers[2].id, 'd1')
      })

      it('should not return matching nested layers containing matching deeper-level nested layers multiple times', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'ABC',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'name': 'ABC',
                  'layers': [createLayerOctopus({ 'id': 'd', 'name': 'ABC' })],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer.findNestedLayers({ name: 'ABC' }).getLayers()
        strictEqual(nestedLayers.length, 3)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        ok(nestedLayers[2])
        strictEqual(nestedLayers[0].id, 'b')
        strictEqual(nestedLayers[1].id, 'c')
        strictEqual(nestedLayers[2].id, 'd')
      })

      it('should not return matching nested layers from levels deeper than the specified depth', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'layers': [
            createLayerOctopus({
              'id': 'b',
              'name': 'ABC',
              'layers': [
                createLayerOctopus({
                  'id': 'c',
                  'name': 'ABC',
                  'layers': [createLayerOctopus({ 'id': 'd', 'name': 'ABC' })],
                }),
              ],
            }),
          ],
        })

        const layer = new Layer(layerOctopus)

        const nestedLayers = layer
          .findNestedLayers({ name: 'ABC' }, { depth: 2 })
          .getLayers()
        strictEqual(nestedLayers.length, 2)
        ok(nestedLayers[0])
        ok(nestedLayers[1])
        strictEqual(nestedLayers[0].id, 'b')
        strictEqual(nestedLayers[1].id, 'c')
      })
    })
  })

  describe('bitmap assets', () => {
    it('should return the main bitmap of a bitmap layer as a bitmap asset', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'bitmap': {
            'filename': 'a.png',
            'bounds': {
              'left': 10,
              'top': 20,
              'right': 110,
              'bottom': 220,
              'width': 100,
              'height': 200,
            },
          },
        })
      )

      const bitmapAssetDescs = layer.getBitmapAssets()
      strictEqual(bitmapAssetDescs.length, 1)
      deepStrictEqual(bitmapAssetDescs[0], {
        layerIds: ['a'],
        name: 'a.png',
        prerendered: false,
      })
    })

    it('should return the prerendered bitmap of a non-bitmap layer as a prerendered bitmap asset', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'shapeLayer',
          'bitmap': {
            'filename': 'a.png',
            'bounds': {
              'left': 10,
              'top': 20,
              'right': 110,
              'bottom': 220,
              'width': 100,
              'height': 200,
            },
          },
        })
      )

      const bitmapAssetDescs = layer.getBitmapAssets()
      strictEqual(bitmapAssetDescs.length, 1)
      deepStrictEqual(bitmapAssetDescs[0], {
        layerIds: ['a'],
        name: 'a.png',
        prerendered: true,
      })
    })

    it('should return pattern fill bitmaps as bitmap assets', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'shapeLayer',
          'effects': {
            'fills': [
              {
                'pattern': {
                  'filename': 'a.png',
                  'offset': { 'horizontal': 0, 'vertical': 0 },
                  'relativeTo': 'layer',
                  'type': 'fit',
                },
              },
              {
                'pattern': {
                  'filename': 'b.png',
                  'offset': { 'horizontal': 0, 'vertical': 0 },
                  'relativeTo': 'layer',
                  'type': 'fit',
                },
              },
            ],
          },
        })
      )

      const bitmapAssetDescs = layer.getBitmapAssets()
      strictEqual(bitmapAssetDescs.length, 2)
      deepStrictEqual(bitmapAssetDescs[0], {
        layerIds: ['a'],
        name: 'a.png',
        prerendered: false,
      })
      deepStrictEqual(bitmapAssetDescs[1], {
        layerIds: ['a'],
        name: 'b.png',
        prerendered: false,
      })
    })

    it('should return pattern border bitmaps as bitmap assets', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'shapeLayer',
          'effects': {
            'borders': [
              {
                'width': 2,
                'pattern': {
                  'filename': 'a.png',
                  'offset': { 'horizontal': 0, 'vertical': 0 },
                  'relativeTo': 'layer',
                  'type': 'fit',
                },
              },
              {
                'width': 2,
                'pattern': {
                  'filename': 'b.png',
                  'offset': { 'horizontal': 0, 'vertical': 0 },
                  'relativeTo': 'layer',
                  'type': 'fit',
                },
              },
            ],
          },
        })
      )

      const bitmapAssetDescs = layer.getBitmapAssets()
      strictEqual(bitmapAssetDescs.length, 2)
      deepStrictEqual(bitmapAssetDescs[0], {
        layerIds: ['a'],
        name: 'a.png',
        prerendered: false,
      })
      deepStrictEqual(bitmapAssetDescs[1], {
        layerIds: ['a'],
        name: 'b.png',
        prerendered: false,
      })
    })

    describe('nested layers', () => {
      it('should return the main bitmaps of nested bitmap layers as bitmap assets', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'type': 'layer',
                'bitmap': {
                  'filename': 'a.png',
                  'bounds': {
                    'left': 10,
                    'top': 20,
                    'right': 110,
                    'bottom': 220,
                    'width': 100,
                    'height': 200,
                  },
                },
              }),
              createLayerOctopus({
                'id': 'c',
                'type': 'layer',
                'bitmap': {
                  'filename': 'b.png',
                  'bounds': {
                    'left': 10,
                    'top': 20,
                    'right': 110,
                    'bottom': 220,
                    'width': 100,
                    'height': 200,
                  },
                },
              }),
            ],
          })
        )

        const bitmapAssetDescs = layer.getBitmapAssets()
        strictEqual(bitmapAssetDescs.length, 2)
        deepStrictEqual(bitmapAssetDescs[0], {
          layerIds: ['b'],
          name: 'a.png',
          prerendered: false,
        })
        deepStrictEqual(bitmapAssetDescs[1], {
          layerIds: ['c'],
          name: 'b.png',
          prerendered: false,
        })
      })

      it('should return the main bitmaps of nested non-bitmap layers as bitmap assets', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'type': 'shapeLayer',
                'bitmap': {
                  'filename': 'a.png',
                  'bounds': {
                    'left': 10,
                    'top': 20,
                    'right': 110,
                    'bottom': 220,
                    'width': 100,
                    'height': 200,
                  },
                },
              }),
              createLayerOctopus({
                'id': 'c',
                'type': 'shapeLayer',
                'bitmap': {
                  'filename': 'b.png',
                  'bounds': {
                    'left': 10,
                    'top': 20,
                    'right': 110,
                    'bottom': 220,
                    'width': 100,
                    'height': 200,
                  },
                },
              }),
            ],
          })
        )

        const bitmapAssetDescs = layer.getBitmapAssets()
        strictEqual(bitmapAssetDescs.length, 2)
        deepStrictEqual(bitmapAssetDescs[0], {
          layerIds: ['b'],
          name: 'a.png',
          prerendered: true,
        })
        deepStrictEqual(bitmapAssetDescs[1], {
          layerIds: ['c'],
          name: 'b.png',
          prerendered: true,
        })
      })

      it('should return pattern fill bitmaps in nested layers as bitmap assets', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'type': 'shapeLayer',
                'effects': {
                  'fills': [
                    {
                      'pattern': {
                        'filename': 'a.png',
                        'offset': { 'horizontal': 0, 'vertical': 0 },
                        'relativeTo': 'layer',
                        'type': 'fit',
                      },
                    },
                    {
                      'pattern': {
                        'filename': 'b.png',
                        'offset': { 'horizontal': 0, 'vertical': 0 },
                        'relativeTo': 'layer',
                        'type': 'fit',
                      },
                    },
                  ],
                },
              }),
              createLayerOctopus({
                'id': 'c',
                'name': 'C',
                'type': 'shapeLayer',
                'effects': {
                  'fills': [
                    {
                      'pattern': {
                        'filename': 'c.png',
                        'offset': { 'horizontal': 0, 'vertical': 0 },
                        'relativeTo': 'layer',
                        'type': 'fit',
                      },
                    },
                  ],
                },
              }),
            ],
          })
        )

        const bitmapAssetDescs = layer.getBitmapAssets()
        strictEqual(bitmapAssetDescs.length, 3)
        deepStrictEqual(bitmapAssetDescs[0], {
          layerIds: ['b'],
          name: 'a.png',
          prerendered: false,
        })
        deepStrictEqual(bitmapAssetDescs[1], {
          layerIds: ['b'],
          name: 'b.png',
          prerendered: false,
        })
        deepStrictEqual(bitmapAssetDescs[2], {
          layerIds: ['c'],
          name: 'c.png',
          prerendered: false,
        })
      })

      it('should return pattern border bitmaps in nested layers as bitmap assets', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'type': 'shapeLayer',
                'effects': {
                  'borders': [
                    {
                      'width': 2,
                      'pattern': {
                        'filename': 'a.png',
                        'offset': { 'horizontal': 0, 'vertical': 0 },
                        'relativeTo': 'layer',
                        'type': 'fit',
                      },
                    },
                    {
                      'width': 2,
                      'pattern': {
                        'filename': 'b.png',
                        'offset': { 'horizontal': 0, 'vertical': 0 },
                        'relativeTo': 'layer',
                        'type': 'fit',
                      },
                    },
                  ],
                },
              }),
              createLayerOctopus({
                'id': 'c',
                'type': 'shapeLayer',
                'effects': {
                  'borders': [
                    {
                      'width': 2,
                      'pattern': {
                        'filename': 'c.png',
                        'offset': { 'horizontal': 0, 'vertical': 0 },
                        'relativeTo': 'layer',
                        'type': 'fit',
                      },
                    },
                  ],
                },
              }),
            ],
          })
        )

        const bitmapAssetDescs = layer.getBitmapAssets()
        strictEqual(bitmapAssetDescs.length, 3)
        deepStrictEqual(bitmapAssetDescs[0], {
          layerIds: ['b'],
          name: 'a.png',
          prerendered: false,
        })
        deepStrictEqual(bitmapAssetDescs[1], {
          layerIds: ['b'],
          name: 'b.png',
          prerendered: false,
        })
        deepStrictEqual(bitmapAssetDescs[2], {
          layerIds: ['c'],
          name: 'c.png',
          prerendered: false,
        })
      })

      it('should not return bitmap assets from nested layers at levels deeper than the specified depth', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'type': 'layer',
                'bitmap': {
                  'filename': 'a.png',
                  'bounds': {
                    'left': 10,
                    'top': 20,
                    'right': 110,
                    'bottom': 220,
                    'width': 100,
                    'height': 200,
                  },
                },
              }),
              createLayerOctopus({
                'id': 'c',
                'type': 'groupLayer',
                'layers': [
                  {
                    'id': 'd',
                    'name': 'D',
                    'type': 'layer',
                    'bitmap': {
                      'filename': 'b.png',
                      'bounds': {
                        'left': 10,
                        'top': 20,
                        'right': 110,
                        'bottom': 220,
                        'width': 100,
                        'height': 200,
                      },
                    },
                  },
                ],
              }),
            ],
          })
        )

        const bitmapAssetDescs = layer.getBitmapAssets({ depth: 2 })
        strictEqual(bitmapAssetDescs.length, 1)
        deepStrictEqual(bitmapAssetDescs[0], {
          layerIds: ['b'],
          name: 'a.png',
          prerendered: false,
        })
      })
    })
  })

  describe('fonts', () => {
    it('should return the default text style font', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'text': {
            'value': 'Oh my text',
            'defaultStyle': {
              'font': {
                'name': 'Abc',
                'type': 'Black',
                'postScriptName': 'PostAbc',
                'syntheticPostScriptName': false,
              },
            },
          },
        })
      )

      const fontDescs = layer.getFonts()
      strictEqual(fontDescs.length, 1)
      deepStrictEqual(fontDescs[0], {
        layerIds: ['a'],
        fontPostScriptName: 'PostAbc',
        fontPostScriptNameSynthetic: false,
        fontTypes: ['Black'],
      })
    })

    it('should flag synthetic default text style font postscript names', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'text': {
            'value': 'Oh my text',
            'defaultStyle': {
              'font': {
                'name': 'Abc',
                'type': 'Black',
                'postScriptName': 'PostAbc',
                'syntheticPostScriptName': true,
              },
            },
          },
        })
      )

      const fontDescs = layer.getFonts()
      strictEqual(fontDescs.length, 1)
      strictEqual(fontDescs[0]?.fontPostScriptNameSynthetic, true)
    })

    it('should return text range style fonts', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'textLayer',
          'text': {
            'value': 'Oh my text',
            'styles': [
              {
                'ranges': [{ 'from': 0, 'to': 10 }],
                'font': {
                  'name': 'Abc',
                  'type': 'Black',
                  'postScriptName': 'PostAbc',
                  'size': 10,
                },
              },
            ],
          },
        })
      )

      const fontDescs = layer.getFonts()
      strictEqual(fontDescs.length, 1)
      deepStrictEqual(fontDescs[0], {
        layerIds: ['a'],
        fontPostScriptName: 'PostAbc',
        fontPostScriptNameSynthetic: false,
        fontTypes: ['Black'],
      })
    })

    it('should flag synthetic text range style font postscript names', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'textLayer',
          'text': {
            'value': 'Oh my text',
            'styles': [
              {
                'ranges': [{ 'from': 0, 'to': 10 }],
                'font': {
                  'name': 'Abc',
                  'type': 'Black',
                  'postScriptName': 'PostAbc',
                  'syntheticPostScriptName': true,
                  'size': 10,
                },
              },
            ],
          },
        })
      )

      const fontDescs = layer.getFonts()
      strictEqual(fontDescs.length, 1)
      strictEqual(fontDescs[0]?.fontPostScriptNameSynthetic, true)
    })

    it('should merge multiple types of the same font into a single item', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'textLayer',
          'text': {
            'value': 'Oh my text',
            'styles': [
              {
                'ranges': [{ 'from': 0, 'to': 10 }],
                'font': {
                  'name': 'Abc',
                  'type': 'Black',
                  'postScriptName': 'PostAbc',
                  'size': 10,
                },
              },
              {
                'ranges': [{ 'from': 10, 'to': 20 }],
                'font': {
                  'name': 'Abc',
                  'type': 'Normal',
                  'postScriptName': 'PostAbc',
                  'size': 10,
                },
              },
            ],
          },
        })
      )

      const fontDescs = layer.getFonts()
      strictEqual(fontDescs.length, 1)
      deepStrictEqual(fontDescs[0], {
        layerIds: ['a'],
        fontPostScriptName: 'PostAbc',
        fontPostScriptNameSynthetic: false,
        fontTypes: ['Black', 'Normal'],
      })
    })

    describe('nested layers', () => {
      it('should return the default text style font from nested layers', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'type': 'textLayer',
                'text': {
                  'value': 'Oh my text',
                  'defaultStyle': {
                    'font': {
                      'name': 'Abc',
                      'type': 'Black',
                      'postScriptName': 'PostAbc',
                      'syntheticPostScriptName': false,
                      'size': 10,
                    },
                  },
                },
              }),
            ],
          })
        )

        const fontDescs = layer.getFonts()
        strictEqual(fontDescs.length, 1)
        deepStrictEqual(fontDescs[0], {
          layerIds: ['b'],
          fontPostScriptName: 'PostAbc',
          fontPostScriptNameSynthetic: false,
          fontTypes: ['Black'],
        })
      })

      it('should return text range style fonts from nested layers', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'name': 'B',
                'type': 'textLayer',
                'text': {
                  'value': 'Oh my text',
                  'styles': [
                    {
                      'ranges': [{ 'from': 0, 'to': 10 }],
                      'font': {
                        'name': 'Abc',
                        'type': 'Black',
                        'postScriptName': 'PostAbc',
                        'size': 10,
                      },
                    },
                  ],
                },
              }),
            ],
          })
        )

        const fontDescs = layer.getFonts()
        strictEqual(fontDescs.length, 1)
        deepStrictEqual(fontDescs[0], {
          layerIds: ['b'],
          fontPostScriptName: 'PostAbc',
          fontPostScriptNameSynthetic: false,
          fontTypes: ['Black'],
        })
      })
    })
  })

  describe('masks', () => {
    it('should claim it is masked when the clipped flag is set', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'clipped': true,
          'maskedBy': 'b',
        })
      )

      strictEqual(layer.isMasked(), true)
    })

    it('should claim it is not masked when the clipped flag is not set', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'clipped': false,
          'maskedBy': undefined,
        })
      )

      strictEqual(layer.isMasked(), false)
    })

    it('should return the mask layer from octopus', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'clipped': true,
          'maskedBy': 'b',
        })
      )

      strictEqual(layer.getMaskLayerId(), 'b')
    })

    it('should not return a mask layer ID when not set in octopus', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'type': 'layer',
          'clipped': false,
          'maskedBy': undefined,
        })
      )

      strictEqual(layer.getMaskLayerId(), null)
    })

    describe('mask layers', () => {
      it('should not return a mask layer when a mask layer is not set in octopus', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'clipped': false,
          'maskedBy': undefined,
        })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          artboard: new Artboard('x', artboardOctopus),
        })

        strictEqual(layer.getMaskLayer(), null)
      })

      it('should not return a mask layer when neither a mask layer is set in octopus nor an artboard instance is provided', () => {
        const layer = new Layer(
          createLayerOctopus({
            'id': 'a',
            'clipped': false,
            'maskedBy': undefined,
          }),
          {
            artboard: null,
          }
        )

        strictEqual(layer.getMaskLayer(), null)
      })

      it('should fail on looking up the mask layer when a mask layer ID is set in octopus without providing the artboard instance', () => {
        const layer = new Layer(
          createLayerOctopus({ 'id': 'a', 'clipped': true, 'maskedBy': 'b' }),
          {
            artboard: null,
          }
        )

        throws(() => {
          layer.getMaskLayer()
        })
      })

      it('should fail on looking up the mask layer when the mask layer is not available on the artboard instance', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'clipped': true,
          'maskedBy': 'b',
        })
        const artboardOctopus = createOctopus({
          'layers': [layerOctopus],
        })

        const layer = new Layer(layerOctopus, {
          artboard: new Artboard('x', artboardOctopus),
        })

        throws(() => {
          layer.getMaskLayer()
        })
      })

      it('should return the mask layer from the artboard instance', () => {
        const layerOctopus = createLayerOctopus({
          'id': 'a',
          'clipped': true,
          'maskedBy': 'b',
        })
        const artboardOctopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'b',
            }),
            layerOctopus,
          ],
        })

        const artboard = new Artboard('x', artboardOctopus)
        const layer = new Layer(layerOctopus, {
          artboard,
        })

        strictEqual(layer.getMaskLayer(), artboard.getLayerById('b'))
      })
    })
  })

  describe('components', () => {
    it('should claim it is a component instance when it has a symbol ID set in octopus', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'symbolID': 'x',
        })
      )

      strictEqual(layer.isComponentInstance(), true)
    })

    it('should claim it is a component instance when it has a document ID set in octopus', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'documentId': 'y',
        })
      )

      strictEqual(layer.isComponentInstance(), true)
    })

    it('should claim it is not a component instance when it has neither a symbol ID nor a document ID set in octopus', () => {
      const layer = new Layer(
        createLayerOctopus({
          'id': 'a',
          'symbolID': undefined,
          'documentId': undefined,
        })
      )

      strictEqual(layer.isComponentInstance(), false)
    })
  })
})
Example #20
Source File: design.spec.ts    From open-design-sdk with Apache License 2.0 4 votes vote down vote up
describe('Design', () => {
  function createOctopus() {
    const width = Math.round(Math.random() * 400)
    const height = Math.round(Math.random() * 400)

    return {
      'frame': {
        'x': Math.round(Math.random() * 400),
        'y': Math.round(Math.random() * 400),
      },
      'bounds': {
        'left': 0,
        'top': 0,
        'right': width,
        'bottom': height,
        'width': width,
        'height': height,
      },
      'layers': [],
    }
  }

  it('should return an added artboard by ID', () => {
    const design = new Design()

    design.addArtboard('a', createOctopus())

    const artboard = design.getArtboardById('a')
    ok(artboard)
    strictEqual(artboard.id, 'a')
  })

  it('should not return a removed artboard by ID', () => {
    const design = new Design()

    design.addArtboard('a', createOctopus())
    design.removeArtboard('a')

    const artboard = design.getArtboardById('a')
    strictEqual(artboard, null)
  })

  describe('artboard list', () => {
    it('should return an empty artboard list by default', () => {
      const design = new Design()

      strictEqual(design.getArtboards().length, 0)
    })

    it('should include an added artboard in the artboard list', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus())

      const artboards = design.getArtboards()
      strictEqual(artboards.length, 1)
      ok(artboards[0])
      strictEqual(artboards[0].id, 'a')
    })

    it('should not include newly added artboards in a previously returned artboard list', () => {
      const design = new Design()

      const prevArtboards = design.getArtboards()

      design.addArtboard('a', createOctopus())
      strictEqual(prevArtboards.length, 0)
    })

    it('should include newly added artboards in the current artboard list', () => {
      const design = new Design()
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const prevArtboards = design.getArtboards()

      design.addArtboard('a', createOctopus())

      const nextArtboards = design.getArtboards()
      strictEqual(nextArtboards.length, 1)
      ok(nextArtboards[0])
      strictEqual(nextArtboards[0].id, 'a')
    })

    it('should keep newly removed artboards in a previously returned artboard list', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus())

      const prevArtboards = design.getArtboards()
      design.removeArtboard('a')

      strictEqual(prevArtboards.length, 1)
      ok(prevArtboards[0])
      strictEqual(prevArtboards[0].id, 'a')
    })

    it('should not include newly removed artboards in the current artboard list', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus())

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const prevArtboards = design.getArtboards()
      design.removeArtboard('a')

      const nextArtboards = design.getArtboards()
      strictEqual(nextArtboards.length, 0)
    })
  })

  describe('page-specific artboard lists', () => {
    it('should return empty page artboard lists by default', () => {
      const design = new Design()

      strictEqual(design.getPageArtboards('p1').length, 0)
    })

    it('should include added artboards in their respective page artboard lists', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { pageId: 'p1' })
      design.addArtboard('b', createOctopus(), { pageId: 'p2' })

      const pageArtboards1 = design.getPageArtboards('p1')
      strictEqual(pageArtboards1.length, 1)
      ok(pageArtboards1[0])
      strictEqual(pageArtboards1[0].id, 'a')

      const pageArtboards2 = design.getPageArtboards('p2')
      strictEqual(pageArtboards2.length, 1)
      ok(pageArtboards2[0])
      strictEqual(pageArtboards2[0].id, 'b')
    })

    it('should not include newly added artboards in a previously returned page artboard list', () => {
      const design = new Design()

      const prevArtboards = design.getPageArtboards('p1')

      design.addArtboard('a', createOctopus(), { pageId: 'p1' })
      strictEqual(prevArtboards.length, 0)
    })

    it('should include newly added artboards in the current artboard list', () => {
      const design = new Design()
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const prevArtboards = design.getPageArtboards('p1')

      design.addArtboard('a', createOctopus(), { pageId: 'p1' })

      const nextArtboards = design.getPageArtboards('p1')
      strictEqual(nextArtboards.length, 1)
      ok(nextArtboards[0])
      strictEqual(nextArtboards[0].id, 'a')
    })
  })

  describe('master component artboards', () => {
    it('should return an added master component artboard by component ID', () => {
      const design = new Design()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })

      const artboard = design.getArtboardByComponentId('abc')
      ok(artboard)
      strictEqual(artboard.id, 'a')
    })

    it('should not return a removed master component artboard by component ID', () => {
      const design = new Design()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })
      design.removeArtboard('a')

      const artboard = design.getArtboardByComponentId('abc')
      strictEqual(artboard, null)
    })
  })

  describe('master component artboard list', () => {
    it('should return an empty master component artboard list by default', () => {
      const design = new Design()

      strictEqual(design.getComponentArtboards().length, 0)
    })

    it('should include an added artboard in the master component artboard list', () => {
      const design = new Design()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })

      const artboards = design.getComponentArtboards()
      strictEqual(artboards.length, 1)
      ok(artboards[0])
      strictEqual(artboards[0].id, 'a')
    })

    it('should not include newly added artboards in a previously returned master component artboard list', () => {
      const design = new Design()

      const prevArtboards = design.getComponentArtboards()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })
      strictEqual(prevArtboards.length, 0)
    })

    it('should include newly added artboards in the current master component artboard list', () => {
      const design = new Design()
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const prevArtboards = design.getComponentArtboards()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })

      const nextArtboards = design.getComponentArtboards()
      strictEqual(nextArtboards.length, 1)
      ok(nextArtboards[0])
      strictEqual(nextArtboards[0].id, 'a')
    })

    it('should keep newly removed artboards in a previously returned master component artboard list', () => {
      const design = new Design()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })

      const prevArtboards = design.getComponentArtboards()
      design.removeArtboard('a')

      strictEqual(prevArtboards.length, 1)
      ok(prevArtboards[0])
      strictEqual(prevArtboards[0].id, 'a')
    })

    it('should not include newly removed artboards in the current master component artboard list', () => {
      const design = new Design()

      design.addArtboard('a', { ...createOctopus(), 'symbolID': 'abc' })

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const prevArtboards = design.getComponentArtboards()
      design.removeArtboard('a')

      const nextArtboards = design.getComponentArtboards()
      strictEqual(nextArtboards.length, 0)
    })
  })

  describe('artboard info', () => {
    it('should not configure the artboard name by default', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus())

      const artboard = design.getArtboardById('a')
      ok(artboard)
      strictEqual(artboard.name, null)
    })

    it('should configure the artboard name when specified', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Hello' })

      const artboard = design.getArtboardById('a')
      ok(artboard)
      strictEqual(artboard.name, 'Hello')
    })

    it('should not configure the artboard page ID by default', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus())

      const artboard = design.getArtboardById('a')
      ok(artboard)
      strictEqual(artboard.pageId, null)
    })

    it('should configure the artboard page ID when specified', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { pageId: 'p1' })

      const artboard = design.getArtboardById('a')
      ok(artboard)
      strictEqual(artboard.pageId, 'p1')
    })
  })

  describe('single artboard lookup', () => {
    it('should look up an added artboard by exact name match', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })

      const artboard = design.findArtboard({ name: 'Abc' })
      ok(artboard)
      strictEqual(artboard.id, 'a')
    })

    it('should look up the first added artboard exactly matching one of the listed names', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Def' })

      const artboard = design.findArtboard({ name: ['Abc', 'Def'] })
      ok(artboard)
      strictEqual(artboard.id, 'a')
    })

    it('should look up the first added artboard matching a name pattern', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Def' })

      const artboard = design.findArtboard({ name: /Abc|Def/ })
      ok(artboard)
      strictEqual(artboard.id, 'a')
    })

    it('should not look up any of the added artboards when neither matches the name', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })

      const artboard = design.findArtboard({ name: 'Unknown' })
      strictEqual(artboard, null)
    })

    it('should not look up a removed artboard when matching by name', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.removeArtboard('a')

      const artboard = design.findArtboard({ name: 'Abc' })
      strictEqual(artboard, null)
    })
  })

  describe('multi-artboard lookup', () => {
    it('should look up added artboards by exact name match sorted by addition order', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Def' })
      design.addArtboard('c', createOctopus(), { name: 'Abc' })

      const artboards = design.findArtboards({ name: 'Abc' })
      strictEqual(artboards.length, 2)
      ok(artboards[0])
      ok(artboards[1])
      strictEqual(artboards[0].id, 'a')
      strictEqual(artboards[1].id, 'c')
    })

    it('should look up added artboards exactly matching one of the listed names', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Xyz' })
      design.addArtboard('c', createOctopus(), { name: 'Def' })

      const artboards = design.findArtboards({ name: ['Abc', 'Def'] })
      strictEqual(artboards.length, 2)
      ok(artboards[0])
      ok(artboards[1])
      strictEqual(artboards[0].id, 'a')
      strictEqual(artboards[1].id, 'c')
    })

    it('should look up the first added artboard matching a name pattern', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Xyz' })
      design.addArtboard('c', createOctopus(), { name: 'Def' })

      const artboards = design.findArtboards({ name: /Abc|Def/ })
      strictEqual(artboards.length, 2)
      ok(artboards[0])
      ok(artboards[1])
      strictEqual(artboards[0].id, 'a')
      strictEqual(artboards[1].id, 'c')
    })

    it('should not look up any of the added artboards when neither matches the name', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Def' })

      const artboards = design.findArtboards({ name: 'Unknown' })
      strictEqual(artboards.length, 0)
    })

    it('should not look up removed artboards when matching by name', () => {
      const design = new Design()

      design.addArtboard('a', createOctopus(), { name: 'Abc' })
      design.addArtboard('b', createOctopus(), { name: 'Abc' })
      design.removeArtboard('a')

      const artboards = design.findArtboards({ name: 'Abc' })
      strictEqual(artboards.length, 1)
      ok(artboards[0])
      strictEqual(artboards[0].id, 'b')
    })
  })

  describe('layer lookup defaults', () => {
    it('should not return any individual layers based on an ID by default', () => {
      const design = new Design()

      strictEqual(design.findLayerById('abc'), null)
    })

    it('should not return any layers based on an ID by default', () => {
      const design = new Design()

      strictEqual(design.findLayersById('abc').length, 0)
    })

    it('should not return any individual layers based on a selector by default', () => {
      const design = new Design()

      strictEqual(design.findLayer({ name: 'abc' }), null)
    })

    it('should not return any layers based on a selector by default', () => {
      const design = new Design()

      strictEqual(design.findLayers({ name: 'abc' }).length, 0)
    })
  })

  describe('artboard data aggregation defaults', () => {
    it('should not return any bitmap assets by default', () => {
      const design = new Design()

      strictEqual(design.getBitmapAssets().length, 0)
    })

    it('should not return any fonts by default', () => {
      const design = new Design()

      strictEqual(design.getFonts().length, 0)
    })

    it('should return an empty flattened layer list by default', () => {
      const design = new Design()

      strictEqual(design.getFlattenedLayers().length, 0)
    })
  })
})
Example #21
Source File: artboard.spec.ts    From open-design-sdk with Apache License 2.0 4 votes vote down vote up
describe('Artboard', () => {
  function createOctopus<T extends Partial<OctopusDocument>>(
    data: T
  ): OctopusDocument {
    return {
      'frame': {
        'x': Math.round(Math.random() * 400),
        'y': Math.round(Math.random() * 400),
      },
      'bounds': {
        'left': 0,
        'top': 0,
        'width': Math.round(Math.random() * 400),
        'height': Math.round(Math.random() * 400),
      },
      'layers': [],
      ...data,
    }
  }

  function createLayerOctopus<T extends Partial<LayerOctopusData>>(
    data: T
  ): LayerOctopusData {
    const id = String(Math.round(Math.random() * 400))
    const base = {
      'id': `layer-${id}`,
      'name': `Layer ID=${id}`,
      'type': 'layer',
    }
    const type = data['type'] || base['type']

    // @ts-expect-error Hard to generalize.
    return {
      ...base,
      ...(type === 'textLayer' ? { 'text:': {} } : {}),
      ...(type === 'groupLayer' ? { 'layers:': [] } : {}),
      ...data,
    }
  }

  describe('artboard info', () => {
    it('should have the provided ID', () => {
      const artboard = new Artboard('a', createOctopus({}))

      strictEqual(artboard.id, 'a')
    })

    it('should have the provided octopus', () => {
      const octopus = createOctopus({})
      const artboard = new Artboard('a', { ...octopus })

      deepStrictEqual(artboard.getOctopus(), octopus)
    })

    it('should have the provided name', () => {
      const artboard = new Artboard('a', createOctopus({}), { name: 'Abc' })

      strictEqual(artboard.name, 'Abc')
    })

    it('should not have a name by default', () => {
      const artboard = new Artboard('a', createOctopus({}))

      strictEqual(artboard.name, null)
    })

    it('should have the provided page ID', () => {
      const artboard = new Artboard('a', createOctopus({}), { pageId: 'p1' })

      strictEqual(artboard.pageId, 'p1')
    })

    it('should not have a page ID by default', () => {
      const artboard = new Artboard('a', createOctopus({}))

      strictEqual(artboard.pageId, null)
    })

    it('should return the provided design', () => {
      const design = new Design()
      const artboard = new Artboard('a', createOctopus({}), { design })

      strictEqual(artboard.getDesign(), design)
    })
  })

  describe('layer lists', () => {
    it('should return a root layer list based on layers in octopus', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({ 'id': 'x' }),
          createLayerOctopus({ 'id': 'a' }),
          createLayerOctopus({ 'id': 'o' }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      const rootLayers = artboard.getRootLayers().getLayers()

      strictEqual(rootLayers.length, 3)
      ok(rootLayers[0])
      ok(rootLayers[1])
      ok(rootLayers[2])
      strictEqual(rootLayers[0].id, 'x')
      strictEqual(rootLayers[1].id, 'a')
      strictEqual(rootLayers[2].id, 'o')
    })

    it('should return the same root layer list on multiple getter calls', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({}),
          createLayerOctopus({}),
          createLayerOctopus({}),
        ],
      })
      const artboard = new Artboard('a', octopus)

      const rootLayers1 = artboard.getRootLayers()
      const rootLayers2 = artboard.getRootLayers()
      strictEqual(rootLayers1, rootLayers2)
    })

    it('should return a flattened layer list based on layers in octopus', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({ 'id': 'a' }),
          createLayerOctopus({
            'id': 'b',
            'type': 'groupLayer',
            'layers': [createLayerOctopus({ 'id': 'a2' })],
          }),
          createLayerOctopus({
            'id': 'c',
            'type': 'groupLayer',
            'layers': [
              createLayerOctopus({ 'id': 'b2' }),
              createLayerOctopus({ 'id': 'c2' }),
            ],
          }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      const flattenedLayers = artboard.getFlattenedLayers().getLayers()

      strictEqual(flattenedLayers.length, 6)
      strictEqual(flattenedLayers[0]?.id, 'a')
      strictEqual(flattenedLayers[1]?.id, 'b')
      strictEqual(flattenedLayers[2]?.id, 'a2')
      strictEqual(flattenedLayers[3]?.id, 'c')
      strictEqual(flattenedLayers[4]?.id, 'b2')
      strictEqual(flattenedLayers[5]?.id, 'c2')
    })

    it('should return the same flattened layer list on multiple getter calls', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({ 'id': 'a' }),
          createLayerOctopus({
            'id': 'b',
            'type': 'groupLayer',
            'layers': [createLayerOctopus({ 'id': 'c' })],
          }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      const flattenedLayers1 = artboard.getFlattenedLayers()
      const flattenedLayers2 = artboard.getFlattenedLayers()
      strictEqual(flattenedLayers1, flattenedLayers2)
    })
  })

  describe('layer lookup', () => {
    describe('ID-based layer lookup', () => {
      it('should look up a root layer by ID', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a' }),
            createLayerOctopus({ 'id': 'b' }),
            createLayerOctopus({ 'id': 'c' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(artboard.getLayerById('b')?.id, 'b')
      })

      it('should look up a first-level nested layer by ID', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a' }),
            createLayerOctopus({
              'id': 'b',
              'type': 'groupLayer',
              'layers': [createLayerOctopus({ 'id': 'c' })],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(artboard.getLayerById('c')?.id, 'c')
      })

      it('should look up a deeper-level nested layer by ID', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'type': 'groupLayer',
                  'layers': [createLayerOctopus({ 'id': 'c' })],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(artboard.getLayerById('c')?.id, 'c')
      })
    })

    describe('selector-based individual layer lookup', () => {
      it('should look up a root layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(artboard.findLayer({ name: 'Layer B' })?.id, 'b')
      })

      it('should look up a first-level nested layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'type': 'groupLayer',
              'layers': [createLayerOctopus({ 'id': 'c', 'name': 'Layer C' })],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(artboard.findLayer({ name: 'Layer C' })?.id, 'c')
      })

      it('should look up a deeper-level nested layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'name': 'Layer B',
                  'type': 'groupLayer',
                  'layers': [
                    createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                  ],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(artboard.findLayer({ name: 'Layer C' })?.id, 'c')
      })

      it('should look up a root layer by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(
          artboard.findLayer({ name: ['Layer C', 'Layer B'] })?.id,
          'b'
        )
      })

      it('should look up a first-level nested layer by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'type': 'groupLayer',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(
          artboard.findLayer({ name: ['Layer D', 'Layer C'] })?.id,
          'c'
        )
      })

      it('should look up a deeper-level nested layer by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'name': 'Layer B',
                  'type': 'groupLayer',
                  'layers': [
                    createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                    createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                  ],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        strictEqual(
          artboard.findLayer({ name: ['Layer D', 'Layer C'] })?.id,
          'c'
        )
      })
    })

    describe('batch selector-based layer lookup', () => {
      it('should look up a root layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer A' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersA = artboard.findLayers({ name: 'Layer A' })
        strictEqual(layersA.length, 2)
        strictEqual(layersA.getLayerById('a')?.id, 'a')
        strictEqual(layersA.getLayerById('c')?.id, 'c')
      })

      it('should look up first-level nested layers by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'type': 'groupLayer',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                createLayerOctopus({ 'id': 'e', 'name': 'Layer C' }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersC = artboard.findLayers({ name: 'Layer C' })
        strictEqual(layersC.length, 2)
        strictEqual(layersC.getLayerById('c')?.id, 'c')
        strictEqual(layersC.getLayerById('e')?.id, 'e')
      })

      it('should look up a deeper-level nested layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'name': 'Layer B',
                  'type': 'groupLayer',
                  'layers': [
                    createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                    createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                    createLayerOctopus({ 'id': 'e', 'name': 'Layer C' }),
                  ],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersC = artboard.findLayers({ name: 'Layer C' })
        strictEqual(layersC.length, 2)
        strictEqual(layersC.getLayerById('c')?.id, 'c')
        strictEqual(layersC.getLayerById('e')?.id, 'e')
      })

      it('should look up root layers by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersAC = artboard.findLayers({ name: ['Layer C', 'Layer A'] })
        strictEqual(layersAC.length, 2)
        strictEqual(layersAC.getLayerById('a')?.id, 'a')
        strictEqual(layersAC.getLayerById('c')?.id, 'c')
      })

      it('should look up first-level nested layers by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'type': 'groupLayer',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                createLayerOctopus({ 'id': 'e', 'name': 'Layer E' }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersCD = artboard.findLayers({ name: ['Layer D', 'Layer C'] })
        strictEqual(layersCD.length, 2)
        strictEqual(layersCD.getLayerById('c')?.id, 'c')
        strictEqual(layersCD.getLayerById('d')?.id, 'd')
      })

      it('should look up deeper-level nested layers by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'name': 'Layer B',
                  'type': 'groupLayer',
                  'layers': [
                    createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                    createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                    createLayerOctopus({ 'id': 'e', 'name': 'Layer E' }),
                  ],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersCD = artboard.findLayers({ name: ['Layer D', 'Layer C'] })
        strictEqual(layersCD.length, 2)
        strictEqual(layersCD.getLayerById('c')?.id, 'c')
        strictEqual(layersCD.getLayerById('d')?.id, 'd')
      })

      it('should not return duplicate results when both a group layer and layers nested within it are matched', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
                createLayerOctopus({ 'id': 'c', 'name': 'Layer A' }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersABC = artboard.findLayers({ id: ['a', 'b', 'c'] })
        strictEqual(layersABC.length, 3)
        strictEqual(layersABC.getLayerById('a')?.id, 'a')
        strictEqual(layersABC.getLayerById('b')?.id, 'b')
        strictEqual(layersABC.getLayerById('c')?.id, 'c')
      })
    })

    describe('batch bitmap asset name selector-based layer lookup', () => {
      it('should look up a root layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer A' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersA = artboard.findLayers({ name: 'Layer A' })
        strictEqual(layersA.length, 2)
        strictEqual(layersA.getLayerById('a')?.id, 'a')
        strictEqual(layersA.getLayerById('c')?.id, 'c')
      })

      it('should look up first-level nested layers by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'type': 'groupLayer',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                createLayerOctopus({ 'id': 'e', 'name': 'Layer C' }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersC = artboard.findLayers({ name: 'Layer C' })
        strictEqual(layersC.length, 2)
        strictEqual(layersC.getLayerById('c')?.id, 'c')
        strictEqual(layersC.getLayerById('e')?.id, 'e')
      })

      it('should look up a deeper-level nested layer by an exact match selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'name': 'Layer B',
                  'type': 'groupLayer',
                  'layers': [
                    createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                    createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                    createLayerOctopus({ 'id': 'e', 'name': 'Layer C' }),
                  ],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersC = artboard.findLayers({ name: 'Layer C' })
        strictEqual(layersC.length, 2)
        strictEqual(layersC.getLayerById('c')?.id, 'c')
        strictEqual(layersC.getLayerById('e')?.id, 'e')
      })

      it('should look up root layers by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
            createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersAC = artboard.findLayers({ name: ['Layer C', 'Layer A'] })
        strictEqual(layersAC.length, 2)
        strictEqual(layersAC.getLayerById('a')?.id, 'a')
        strictEqual(layersAC.getLayerById('c')?.id, 'c')
      })

      it('should look up first-level nested layers by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
            createLayerOctopus({
              'id': 'b',
              'name': 'Layer B',
              'type': 'groupLayer',
              'layers': [
                createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                createLayerOctopus({ 'id': 'e', 'name': 'Layer E' }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersCD = artboard.findLayers({ name: ['Layer D', 'Layer C'] })
        strictEqual(layersCD.length, 2)
        strictEqual(layersCD.getLayerById('c')?.id, 'c')
        strictEqual(layersCD.getLayerById('d')?.id, 'd')
      })

      it('should look up deeper-level nested layers by a list selector', () => {
        const octopus = createOctopus({
          'layers': [
            createLayerOctopus({
              'id': 'a',
              'name': 'Layer A',
              'layers': [
                createLayerOctopus({
                  'id': 'b',
                  'name': 'Layer B',
                  'type': 'groupLayer',
                  'layers': [
                    createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                    createLayerOctopus({ 'id': 'd', 'name': 'Layer D' }),
                    createLayerOctopus({ 'id': 'e', 'name': 'Layer E' }),
                  ],
                }),
              ],
            }),
          ],
        })
        const artboard = new Artboard('a', octopus)

        const layersCD = artboard.findLayers({ name: ['Layer D', 'Layer C'] })
        strictEqual(layersCD.length, 2)
        strictEqual(layersCD.getLayerById('c')?.id, 'c')
        strictEqual(layersCD.getLayerById('d')?.id, 'd')
      })
    })
  })

  describe('layer collection flattening', () => {
    it('should not return duplicate results when both a group layer and layers nested within it are matched', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({
            'id': 'a',
            'name': 'Layer A',
            'layers': [
              createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
              createLayerOctopus({ 'id': 'c', 'name': 'Layer A' }),
            ],
          }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      const layersABC = artboard.findLayers({ id: ['a', 'b', 'c'] })
      const flattened = layersABC.flatten()
      strictEqual(flattened.length, 3)
      strictEqual(flattened.getLayerById('a')?.id, 'a')
      strictEqual(flattened.getLayerById('b')?.id, 'b')
      strictEqual(flattened.getLayerById('c')?.id, 'c')
    })
  })

  describe('layer depth info', () => {
    it('should return the depth of 0 for root layers', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
          createLayerOctopus({ 'id': 'b', 'name': 'Layer B' }),
          createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.getLayerDepth('b'), 0)
    })

    it('should return the depth of 1 for first-level nested layers', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({ 'id': 'a', 'name': 'Layer A' }),
          createLayerOctopus({
            'id': 'b',
            'name': 'Layer B',
            'type': 'groupLayer',
            'layers': [createLayerOctopus({ 'id': 'c', 'name': 'Layer C' })],
          }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.getLayerDepth('c'), 1)
    })

    it('should return depth for deeper-level nested layers', () => {
      const octopus = createOctopus({
        'layers': [
          createLayerOctopus({
            'id': 'a',
            'name': 'Layer A',
            'layers': [
              createLayerOctopus({
                'id': 'b',
                'name': 'Layer B',
                'type': 'groupLayer',
                'layers': [
                  createLayerOctopus({ 'id': 'c', 'name': 'Layer C' }),
                ],
              }),
            ],
          }),
        ],
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.getLayerDepth('c'), 2)
    })
  })

  describe('artboard background', () => {
    it('should not return a background color when no color is specified in octopus', () => {
      const octopus = createOctopus({
        'hasBackgroundColor': false,
        'backgroundColor': undefined,
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.getBackgroundColor(), null)
    })

    it('should not return the background color from octopus when the background color is disabled', () => {
      const octopus = createOctopus({
        'hasBackgroundColor': false,
        'backgroundColor': { 'r': 50, 'g': 150, 'b': 250, 'a': 1 },
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.getBackgroundColor(), null)
    })

    it('should return the background color from octopus when the background color is enabled', () => {
      const octopus = createOctopus({
        'hasBackgroundColor': true,
        'backgroundColor': { 'r': 50, 'g': 150, 'b': 250, 'a': 1 },
      })
      const artboard = new Artboard('a', octopus)

      deepStrictEqual(artboard.getBackgroundColor(), {
        'r': 50,
        'g': 150,
        'b': 250,
        'a': 1,
      })
    })
  })

  describe('component artboards', () => {
    it('should claim the artboard is a component when it has a symbol ID in octopus', () => {
      const octopus = createOctopus({
        'symbolID': 'x',
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.isComponent(), true)
    })

    it('should not claim the artboard is a component when it does not have a symbol ID in octopus', () => {
      const octopus = createOctopus({
        'symbolID': undefined,
      })
      const artboard = new Artboard('a', octopus)

      strictEqual(artboard.isComponent(), false)
    })
  })
})