src/components/OrgFile/OrgFile.unit.test.js
/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectStrippedDescription", "expectType"] }] */
import {
parseOrg,
parseDescriptionPrefixElements,
_parsePlanningItems,
parseMarkupAndCookies,
} from '../../lib/parse_org';
import { exportOrg, createRawDescriptionText } from '../../lib/export_org';
import { newHeaderWithTitle } from '../../lib/parse_org';
import readFixture from '../../../test_helpers/index';
import { noLogRepeatEnabledP } from '../../reducers/org';
import { fromJS } from 'immutable';
/**
* This is a convenience wrapper around parsing an org file using
* `parseOrg` and then export it using `exportOrg`.
* @param {String} testOrgFile - contents of an org file
* @param {Boolean} dontIndent - by default false, so indent drawers
*/
function parseAndExportOrgFile(testOrgFile, dontIndent = false) {
const parsedFile = parseOrg(testOrgFile);
const exportedFile = exportOrg({
headers: parsedFile.get('headers'),
linesBeforeHeadings: parsedFile.get('linesBeforeHeadings'),
dontIndent: dontIndent,
});
return exportedFile;
}
describe('Tests for export', () => {
const createSimpleHeaderWithDescription = (description) =>
newHeaderWithTitle('Test', 1, fromJS([]))
.set('description', fromJS([{ type: 'text', contents: description }]))
.set('rawDescription', description);
test('Simple description export of empty description works', () => {
const description = '';
const header = createSimpleHeaderWithDescription(description);
expect(createRawDescriptionText(header, false, false)).toEqual(description);
});
test('Simple description export of empty line works', () => {
const description = '\n';
const header = createSimpleHeaderWithDescription(description);
expect(createRawDescriptionText(header, false, false)).toEqual(description);
});
test('Simple description export of non-empty line works', () => {
const description = 'abc\n';
const header = createSimpleHeaderWithDescription(description);
expect(createRawDescriptionText(header, false, false)).toEqual(description);
});
test('Simple description export of non-empty line without trailing newline works (newline will be added)', () => {
const description = 'abc';
const header = createSimpleHeaderWithDescription(description);
expect(createRawDescriptionText(header, false, false)).toEqual(`${description}\n`);
});
});
describe('Unit Tests for Org file', () => {
describe('Parsing and export should not alter the description part', () => {
const expectStrippedDescription = (description) => {
const { strippedDescription } = parseDescriptionPrefixElements(description, fromJS([]));
return expect(strippedDescription);
};
test('Parse empty description', () => {
const description = '';
expectStrippedDescription(description).toEqual(description);
});
test('Parse newline description', () => {
const description = '\n';
expectStrippedDescription(description).toEqual(description);
});
test('Parse simple description prefixed with newline', () => {
const description = '\nfoo';
expectStrippedDescription(description).toEqual(description);
});
test('Parse simple description surrounded by newlines', () => {
const description = '\nfoo\n';
expectStrippedDescription(description).toEqual(description);
});
test('Parse simple description with planning item', () => {
const description = 'DEADLINE: <2020-01-01 Mon>';
expectStrippedDescription(description).toEqual('');
});
test('Parse simple description with planning item with newline', () => {
const description = 'DEADLINE: <2020-01-01 Mon> \n';
expectStrippedDescription(description).toEqual('');
});
test('Parse simple description with planning item and more content', () => {
const description = 'DEADLINE: <2020-01-01 Mon> \nfoo\n';
expectStrippedDescription(description).toEqual('foo\n');
});
test('Parse empty description with empty properties', () => {
const description = `:PROPERTIES:
:END:
`;
expectStrippedDescription(description).toEqual('');
});
test('Parse empty line description with properties', () => {
const text = '\n';
const description = `:PROPERTIES:
:END:
${text}`;
expectStrippedDescription(description).toEqual(text);
});
test('Parse simple description with empty properties', () => {
const text = 'abc\n';
const description = `:PROPERTIES:
:END:
${text}`;
expectStrippedDescription(description).toEqual(text);
});
});
describe('Parsing and exporting should not alter the original file', () => {
test("Parsing and exporting shouldn't alter the original file", () => {
const testOrgFile = readFixture('all_the_features');
const exportedFile = parseAndExportOrgFile(testOrgFile);
// Should have the same amount of lines. Safeguard for the next
// expectation.
const exportedFileLines = exportedFile.split('\n');
const testOrgFileLines = testOrgFile.split('\n');
expect(exportedFileLines.length).toEqual(testOrgFileLines.length);
exportedFileLines.forEach((line, index) => {
expect(line).toEqual(testOrgFileLines[index]);
});
});
test('Parse empty file', () => {
const testOrgFile = '';
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parse file with one empty line', () => {
const testOrgFile = '\n';
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual('\n');
});
test('Parse very basic file with one header, no description', () => {
const testOrgFile = '* Header\n';
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parse very basic file with one header, empty line of description', () => {
const testOrgFile = '* Header\n\n';
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parse very basic file with one header, one line of description', () => {
const testOrgFile = '* Header\nabc\n';
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parse a header with PROPERTIES', () => {
const testOrgFile = '* Header\n :PROPERTIES:\n :CUSTOM_ID: link_to_me\n :END:\n';
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parse basic file with description', () => {
const testOrgFile = readFixture('bold_text');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parse basic file with list', () => {
const testOrgFile = readFixture('indented_list');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parses and exports a file which contains all features of organice', () => {
const testOrgFile = readFixture('all_the_features');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
describe('Boldness', () => {
test('Parsing lines with bold text', () => {
const testOrgFile = readFixture('bold_text');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
describe('Parsing inline-markup', () => {
test('Parses inline-markup where closing delim is followed by ;', () => {
const result = parseMarkupAndCookies('*bold*;');
expect(result.length).toEqual(2);
});
});
describe('regex collisions of inline-markup and different links', () => {
test('Parse /italic/ followed by URL with /', () => {
const result = parseMarkupAndCookies('/italic/ word http://example.com/ text');
expect(result.length).toEqual(4);
});
test('Parse =verb= followed by URL with = in query', () => {
const result = parseMarkupAndCookies('=URL=: http://example.com/?a=b');
expect(result.length).toEqual(3);
});
});
describe('HTTP URLs', () => {
test('Parse a line containing an URL but no /italic/ text before the URL', () => {
const testOrgFile = readFixture('url');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
describe('www URLs', () => {
const testOrgFile = readFixture('www_url');
test('Parse a line containing an URL starting with www', () => {
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parses all valid URLs starting with www', () => {
const parsedFile = parseOrg(testOrgFile);
const firstHeader = parsedFile.get('headers').first();
const parsedUrls = firstHeader
.get('description')
.filter((x) => x.get('type') === 'www-url');
expect(parsedUrls.size).toEqual(2);
});
});
describe('E-mail address', () => {
test('Parse a line containing an e-mail address', () => {
const testOrgFile = readFixture('email');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
describe('Phone number in canonical format (+xxxxxx)', () => {
test('Parse a line containing a phone number but no +striked+ text after the number', () => {
const testOrgFile = readFixture('phonenumber');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
describe('Newlines', () => {
test('Newlines in between headers and items are preserved', () => {
const testOrgFile = readFixture('newlines');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
test('Config and content lines before first heading line are kept', () => {
const testOrgFile = readFixture('before-first-headline');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
describe('Planning items', () => {
describe('Formatting is the same as in Emacs', () => {
describe('List formatting', () => {
test('Parsing a basic list should not mangle the list', () => {
const testDescription = ' - indented list\n - Foo';
const parsedFile = _parsePlanningItems(testDescription);
expect(parsedFile.strippedDescription).toEqual(testDescription);
});
test('Parsing a list with planning items should not mangle the list', () => {
const testDescription = ' - indented list\n - Foo';
const parsedFile = _parsePlanningItems(
`SCHEDULED: <2019-07-30 Tue>\n${testDescription}`
);
expect(parsedFile.strippedDescription).toEqual(testDescription);
});
describe('Parsing planning items must not consume leading spaces in description', () => {
test('Basic', () => {
const description = ' - list item';
const { strippedDescription } = _parsePlanningItems(description);
expect(strippedDescription).toEqual(description);
});
test('Scheduled', () => {
const description = ' - list item';
const completeDescription = `SCHEDULED: <2020-01-01> \n${description}`;
const { strippedDescription } = _parsePlanningItems(completeDescription);
expect(strippedDescription).toEqual(description);
});
});
test('Parsing planning items must only consume one trailing newline', () => {
const description = '\n\nabc';
const completeDescription = `SCHEDULED: <2020-01-01> \n${description}`;
const { strippedDescription } = _parsePlanningItems(completeDescription);
expect(strippedDescription).toEqual(description);
});
test('Parsing planning items should not discard an empty line of description text', () => {
const testOrgFile = `* Header
SCHEDULED: <2019-07-30 Tue>
* Header 2
`;
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parsing planning items must exactly consume one trailing newline', () => {
const description = 'abc\n';
const completeDescription = `
SCHEDULED: <2020-01-01 Wed>
${description}`;
const { strippedDescription } = _parsePlanningItems(completeDescription);
expect(strippedDescription).toEqual(description);
});
test('Parsing planning items should not add an empty line of description text', () => {
const testOrgFile = `* Header
SCHEDULED: <2019-07-30 Tue>
* Header 2
`;
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
describe('Parses planning item with following checkmark', () => {
it('parses and exports without changes', () => {
const testOrgFile = readFixture('planning_item_with_following_checkmark');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Parsing planning items followed by a checklist must work', () => {
const testDescription = '- [ ] foo\n- [ ] bar';
const parsed = _parsePlanningItems(`SCHEDULED: <2019-07-30 Tue>\n${testDescription}`);
const parsedPlanningItem = parsed.planningItems.toJS();
expect(parsedPlanningItem[0].timestamp.dayName).toEqual('Tue');
expect(parsed.strippedDescription).toEqual(testDescription);
});
});
});
describe('Planning items are formatted as is default Emacs', () => {
test('For files with timestamps in title and description', () => {
const testOrgFile = readFixture('schedule_and_timestamps');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('For basic files', () => {
const testOrgFile = readFixture('schedule');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('For files with multiple headlines with timestamps', () => {
const testOrgFile = readFixture('multiple_headlines_with_timestamps');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('For files with multiple planning items', () => {
const testOrgFile = readFixture('schedule_and_deadline');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
test('Properties are formatted as is default in Emacs', () => {
const testOrgFile = readFixture('properties');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Properties are flush-left when dontIndent is true', () => {
const testOrgFile = readFixture('properties');
const exportedLines = parseAndExportOrgFile(testOrgFile, true).split('\n');
expect(exportedLines[2]).toEqual(':PROPERTIES:');
});
test('Tags are formatted as is default in Emacs', () => {
const testOrgFile = readFixture('tags');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
});
describe('Logbook entries', () => {
test('Logbook entries are formatted as is default in Emacs', () => {
const testOrgFile = readFixture('logbook');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
test('Logbook entries are indented by default', () => {
const testOrgFile = readFixture('logbook');
const exportedLines = parseAndExportOrgFile(testOrgFile).split('\n');
expect(exportedLines[1]).toEqual(' :LOGBOOK:');
expect(exportedLines[2].startsWith(' CLOCK:')).toBeTruthy();
});
test('Logbook entries are not indented when dontIndent', () => {
const testOrgFile = readFixture('logbook');
const exportedLines = parseAndExportOrgFile(testOrgFile, true).split('\n');
expect(exportedLines[1]).toEqual(':LOGBOOK:');
expect(exportedLines[2].startsWith('CLOCK:')).toBeTruthy();
});
});
});
describe('Log notes followed by a log book', () => {
const testOrgFile = readFixture('logbook_and_log_notes');
test('Parse and export does not change original file', () => {
const exported = parseAndExportOrgFile(testOrgFile);
expect(testOrgFile).toEqual(exported);
});
const parsed = parseOrg(testOrgFile).toJS();
test('Log notes of first headline are parsed', () => {
expect(parsed.headers[0].logNotes[0].type).toEqual('list');
expect(parsed.headers[0].logNotes[0].items.length).toEqual(1);
});
test('Log notes of second headline are parsed', () => {
expect(parsed.headers[1].logNotes[0].type).toEqual('list');
expect(parsed.headers[1].logNotes[0].items.length).toEqual(4);
});
});
describe('Reducers and helper functions', () => {
describe('"nologrepeat" configuration', () => {
test('Detects "nologrepeat" when set in #+STARTUP as only option', () => {
const testOrgFile = readFixture('schedule_with_repeater_and_nologrepeat');
const state = parseOrg(testOrgFile);
expect(noLogRepeatEnabledP({ state, headerIndex: 0 })).toBe(true);
});
test('Detects "nologrepeat" when set in #+STARTUP with other options', () => {
const testOrgFile = readFixture('schedule_with_repeater_and_nologrepeat_and_other_options');
const state = parseOrg(testOrgFile);
expect(noLogRepeatEnabledP({ state, headerIndex: 0 })).toBe(true);
});
test('Does not detect "nologrepeat" when not set', () => {
const testOrgFile = readFixture('schedule_with_repeater');
const state = parseOrg(testOrgFile);
expect(noLogRepeatEnabledP({ state, headerIndex: 0 })).toBe(false);
});
test('Detects "nologrepeat" when set via a property list', () => {
const testOrgFile = readFixture('schedule_with_repeater_and_nologrepeat_property');
const state = parseOrg(testOrgFile);
expect(noLogRepeatEnabledP({ state, headerIndex: 1 })).toBe(true);
expect(noLogRepeatEnabledP({ state, headerIndex: 2 })).toBe(true);
expect(noLogRepeatEnabledP({ state, headerIndex: 5 })).toBe(false);
expect(noLogRepeatEnabledP({ state, headerIndex: 7 })).toBe(true);
});
});
});
describe('TODO keywords at EOF', () => {
test('formatted as in default emacs', () => {
const testOrgFile = readFixture('todo_keywords_interspersed');
const exportedFile = parseAndExportOrgFile(testOrgFile);
expect(exportedFile).toEqual(testOrgFile);
});
});
});