stoplightio/markdown

View on GitHub
src/frontmatter/__tests__/fronmatter.spec.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import * as fs from 'fs';
import { join } from 'path';

import { MDAST } from '../../ast-types';
import { parse } from '../../parse';
import { parseWithPointers } from '../../parseWithPointers';
import { stringify } from '../../stringify';
import { Frontmatter } from '../frontmatter';

const FIXTURES_DIR = join(__dirname, './fixtures');

const tags = fs.readFileSync(join(FIXTURES_DIR, 'tags.md'), 'utf-8');
const invalid = fs.readFileSync(join(FIXTURES_DIR, 'invalid.md'), 'utf-8');

describe('Frontmatter', () => {
  it('should clone given data by default', () => {
    const parsed = parseWithPointers(tags);
    const instance = new Frontmatter(parsed.ast);

    expect(instance.document).not.toBe(parsed.ast);
  });

  it('should throw when invalid ast is provided', () => {
    const node: MDAST.Content = {
      type: 'paragraph',
      value: '',
      children: [],
    };

    // @ts-expect-error expect typing error because providing invalid ast...
    expect(() => new Frontmatter(node)).toThrow();
  });

  describe('no frontmatter fixture', () => {
    const fixture = '**welcome**\n~~test~~\n';
    it('should return undefined when trying to access properties', () => {
      const instance = new Frontmatter(fixture);

      expect(instance.getAll()).toBeUndefined();
      expect(instance.get('test')).toBeUndefined();
    });

    it('getFrontmatterBlock should return undefined', () => {
      expect(Frontmatter.getFrontmatterBlock(fixture)).toBeUndefined();
    });

    describe('set', () => {
      it('should create frontmatter block', () => {
        const instance = new Frontmatter(fixture);

        instance.set('test', 23);
        instance.set('foo', 123);
        expect(instance.getAll()).toEqual({
          test: 23,
          foo: 123,
        });
        expect(instance.stringify()).toEqual(`---
test: 23
foo: 123
---

**welcome**
~~test~~
`);
      });
    });
  });

  describe('invalid fixture', () => {
    it('should return empty object', () => {
      const instance = new Frontmatter(invalid);

      expect(instance.getAll()).toEqual({});
    });

    it('getFrontmatterBlock should return block', () => {
      expect(Frontmatter.getFrontmatterBlock(invalid)).toEqual(`---
tti
tags: [Hubs]
---`);
    });

    describe('set', () => {
      it('should update frontmatter block correctly', () => {
        const instance = new Frontmatter(invalid);

        instance.set('title', 'hello!');
        instance.set('tags', ['test']);
        expect(instance.getAll()).toEqual({
          tags: ['test'],
          title: 'hello!',
        });
        expect(instance.stringify()).toEqual(`---
title: hello!
tags: [test]
---

# Hello world!
`);
      });
    });

    describe('unset', () => {
      it('should do nothing', () => {
        const instance = new Frontmatter(invalid);

        instance.unset('tit');

        expect(instance.getAll()).toEqual({});
      });
    });
  });

  describe('tags fixture', () => {
    it('should get all properties', () => {
      const instance = new Frontmatter(tags);

      expect(instance.getAll()).toEqual({
        tags: ['introductions', 'guides'],
        title: 'Graphite Introduction',
      });
    });

    it('getFrontmatterBlock should return block', () => {
      expect(Frontmatter.getFrontmatterBlock(tags)).toEqual(`---
title: Graphite Introduction
tags: ['introductions', 'guides']
---`);
    });

    describe('#get', () => {
      it('should return value for single item', () => {
        const instance = new Frontmatter(tags);

        expect(instance.get('title')).toEqual('Graphite Introduction');
        expect(instance.get('tags')).toEqual(['introductions', 'guides']);
      });

      it('should support nested properties', () => {
        const instance = new Frontmatter(tags);

        expect(instance.get(['tags', 1])).toEqual('guides');
        expect(instance.get('tags.0')).toEqual('introductions');
      });
    });

    describe('#set', () => {
      it('should update properties', () => {
        const instance = new Frontmatter(tags);

        instance.set('tags', ['foo']);

        expect(instance.get('tags')).toEqual(['foo']);
      });

      it('should update ast', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.set('tags', ['foo']);

        expect(parsed.ast.children[0]!.value).toEqual('title: Graphite Introduction\ntags: [foo]'); // should be quoted?
      });

      it('should support nested properties', () => {
        const instance = new Frontmatter(tags);
        instance.set('tags.0', 'new guides');
        instance.set(['tags', 1], 'start');

        expect(instance.get('tags')).toEqual(['new guides', 'start']);
      });

      it('should re-add previously removed block', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.unset('tags');
        instance.unset('title');

        expect(parsed.ast.children[0]!.type).toEqual('heading'); // verifies block is not there, more decent assertion is done in #unset test case

        instance.set('tags', []);

        expect(parsed.ast.children[0]!.type).toEqual('yaml');
        expect(stringify(parsed.ast)).toEqual(`---
tags: []
---

# Introduction

Coolio.
`);
      });

      it('should add new properties', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.set('new item', 2);

        expect(parsed.ast.children[0]!.value).toEqual(
          'title: Graphite Introduction\ntags: [introductions, guides]\nnew item: 2',
        );
      });

      it('document should be serializable', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.set('tags', ['foo']);

        expect(stringify(parsed.ast)).toEqual(`---
title: Graphite Introduction
tags: [foo]
---

# Introduction

Coolio.
`);
      });
    });

    describe('#unset', () => {
      it('should update properties', () => {
        const instance = new Frontmatter(tags, true);

        instance.unset('tags');

        expect(instance.getAll()).not.toHaveProperty('tags');
      });

      it('should update ast', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.unset('tags');

        expect(parsed.ast.children[0]!.value).toEqual('title: Graphite Introduction');
      });

      it('should remove block if all properties are removed', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.unset('tags');
        instance.unset('title');
        instance.unset('title');

        expect(instance.stringify()).toEqual(`# Introduction

Coolio.
`);
        expect(parsed.ast.children[0]!.type).toEqual('heading');
        expect(parsed.ast.children).toHaveLength(2);
      });

      it('should support nested properties', () => {
        const instance = new Frontmatter(tags);
        instance.unset('tags.0');

        expect(instance.get('tags')).toEqual(['guides']);
      });

      it('should handle non-existing properties gracefully', () => {
        const instance = new Frontmatter(tags);
        instance.unset('tags.3');
        instance.unset('5');
        instance.unset('foo');

        expect(instance.getAll()).toEqual(new Frontmatter(tags).getAll());
      });

      it('should do nothing if property does not exist', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.unset('test');

        expect(instance.getAll()).toEqual({
          tags: ['introductions', 'guides'],
          title: 'Graphite Introduction',
        });
      });

      it('document should be serializable', () => {
        const parsed = parseWithPointers(tags);
        const instance = new Frontmatter(parsed.ast, true);

        instance.unset('tags');

        expect(stringify(parsed.ast)).toEqual(`---
title: Graphite Introduction
---

# Introduction

Coolio.
`);
      });
    });

    it('should expose a way to stringify existing document', () => {
      const instance = new Frontmatter(tags);
      expect(instance.stringify()).toEqual(tags);
    });
  });

  describe('getFrontmatterBlock method', () => {
    it.each(['---', '------', '---\nfoo', '--\n---', '---\na--', '\na\n---'])(
      'should return undefined for incomplete block `%s`',
      document => {
        const block = Frontmatter.getFrontmatterBlock(document);
        expect(block).toBeUndefined();
        expect(parse(document)).toEqual(
          expect.objectContaining({
            type: 'root',
            children: expect.arrayContaining([expect.not.objectContaining({ type: 'yaml' })]),
          }),
        );
      },
    );

    it.each(['---\n---', '---\nfoo\n---'])('should produce the same result for `%s`', () => {
      const document = `---\n---`;
      const block = Frontmatter.getFrontmatterBlock(document)!;

      expect(block).toEqual(document);
      expect(stringify(parse(block))).toEqual(stringify(parse(document)));
    });
  });
});