Skip to content

Commit

Permalink
test: add tests for main functions. Decouple functions for better tes…
Browse files Browse the repository at this point in the history
…tability
  • Loading branch information
bandantonio committed Apr 26, 2024
1 parent a4adc48 commit 2e1d747
Show file tree
Hide file tree
Showing 17 changed files with 345 additions and 129 deletions.
103 changes: 10 additions & 93 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@


import dayjs from 'dayjs';
import * as Handlebars from 'handlebars';
import { normalizePath, Notice, Plugin } from 'obsidian';
import path from 'path';
import { DEFAULT_SETTINGS, IBookHighlightsSettingTab } from './src/settings';
import { IBookHighlightsPluginSearchModal } from './src/search';
import { annotationsRequest, dbRequest } from 'src/db/index';
import {
BOOKS_DB_PATH,
HIGHLIGHTS_DB_PATH,
BOOKS_LIBRARY_NAME,
HIGHLIGHTS_LIBRARY_NAME,
BOOKS_LIBRARY_COLUMNS,
HIGHLIGHTS_LIBRARY_COLUMNS
} from './src/db/constants';
import {
ICombinedBooksAndHighlights,
IBook,
IBookAnnotation,
IBookHighlightsPluginSettings
} from './src/types';
import { aggregateBookAndHighlightDetails } from './src/methods/aggregateDetails';
import { renderHighlightsTemplate } from './src/methods/renderHighlightsTemplate';

export default class IBookHighlightsPlugin extends Plugin {
settings: IBookHighlightsPluginSettings;
Expand All @@ -38,12 +25,12 @@ export default class IBookHighlightsPlugin extends Plugin {
}).catch((error) => {
new Notice(`[${this.manifest.name}]:\nError importing highlights. Check console for details (⌥ ⌘ I)`, 0);
console.error(`[${this.manifest.name}]: ${error}`);

});
});

this.addSettingTab(new IBookHighlightsSettingTab(this.app, this));

this.addCommand({
id: 'import-all-highlights',
name: 'Import all',
Expand All @@ -56,7 +43,7 @@ export default class IBookHighlightsPlugin extends Plugin {
}
},
});

this.addCommand({
id: 'import-single-highlights',
name: 'From a specific book...',
Expand Down Expand Up @@ -84,68 +71,9 @@ export default class IBookHighlightsPlugin extends Plugin {
await this.saveData(this.settings);
}

async getBooks(): Promise<IBook[]> {
const bookDetails = await dbRequest(
BOOKS_DB_PATH,
`SELECT ${BOOKS_LIBRARY_COLUMNS.join(', ')} FROM ${BOOKS_LIBRARY_NAME} WHERE ZPURCHASEDATE IS NOT NULL`
) as IBook[];

return bookDetails;
}

async getAnnotations(): Promise<IBookAnnotation[]> {
const annotationDetails = await annotationsRequest(
HIGHLIGHTS_DB_PATH,
`SELECT ${HIGHLIGHTS_LIBRARY_COLUMNS.join(', ')} FROM ${HIGHLIGHTS_LIBRARY_NAME} WHERE ZANNOTATIONSELECTEDTEXT IS NOT NULL AND ZANNOTATIONDELETED IS 0`
) as IBookAnnotation[];

return annotationDetails;
}

async aggregateBookAndHighlightDetails(): Promise<ICombinedBooksAndHighlights[]> {
const books = await this.getBooks();
const annotations = await this.getAnnotations();

const resultingHighlights: ICombinedBooksAndHighlights[] = books.reduce((highlights: ICombinedBooksAndHighlights[], book: IBook) => {
const bookRelatedAnnotations: IBookAnnotation[] = annotations.filter(annotation => annotation.ZANNOTATIONASSETID === book.ZASSETID);

if (bookRelatedAnnotations.length > 0) {
// Obsidian forbids adding certain characters to the title of a note, so they must be replaced with a dash (-)
// | # ^ [] \ / :
// eslint-disable-next-line
const normalizedBookTitle = book.ZTITLE.replace(/[\|\#\^\[\]\\\/\:]+/g, ' -');

highlights.push({
bookTitle: normalizedBookTitle,
bookId: book.ZASSETID,
bookAuthor: book.ZAUTHOR,
bookGenre: book.ZGENRE,
bookLanguage: book.ZLANGUAGE,
bookLastOpenedDate: book.ZLASTOPENDATE,
bookCoverUrl: book.ZCOVERURL,
annotations: bookRelatedAnnotations.map(annotation => {
return {
chapter: annotation.ZFUTUREPROOFING5,
contextualText: annotation.ZANNOTATIONREPRESENTATIVETEXT,
highlight: annotation.ZANNOTATIONSELECTEDTEXT,
note: annotation.ZANNOTATIONNOTE,
highlightStyle: annotation.ZANNOTATIONSTYLE,
highlightCreationDate: annotation.ZANNOTATIONCREATIONDATE,
highlightModificationDate: annotation.ZANNOTATIONMODIFICATIONDATE
}
})
})
}

return highlights;
}, []);

return resultingHighlights;
}

async aggregateAndSaveHighlights(): Promise<void> {
const highlights = await this.aggregateBookAndHighlightDetails();
const highlights = await aggregateBookAndHighlightDetails();

if (highlights.length === 0) {
throw ('No highlights found. Make sure you made some highlights in your Apple Books.');
}
Expand Down Expand Up @@ -175,20 +103,9 @@ export default class IBookHighlightsPlugin extends Plugin {

await this.app.vault.createFolder(this.settings.highlightsFolder);

highlights.forEach(async (highlight: ICombinedBooksAndHighlights) => {
// TODO: Consider moving to a separate file if there are several helpers to be added
Handlebars.registerHelper('eq', (a, b) => {
if (a == b) {
return this;
}
});

Handlebars.registerHelper('dateFormat', (date, format) => {
return dayjs('2001-01-01').add(date, 's').format(format);
});

const template = Handlebars.compile(this.settings.template);
const renderedTemplate = template(highlight);
highlights.forEach(async (highlight: ICombinedBooksAndHighlights) => {
const renderedTemplate = await renderHighlightsTemplate(highlight, this.settings.template);

await this.app.vault.create(
normalizePath(path.join(this.settings.highlightsFolder, `${highlight.bookTitle}.md`)),
Expand Down
20 changes: 10 additions & 10 deletions src/db/seedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@ export const defaultBooks = [{
"ZGENRE": "Technology",
"ZLANGUAGE": "EN",
"ZLASTOPENDATE": 726602576.413094,
"ZCOVERURL": null
"ZCOVERURL": ''
}, {
"ZASSETID": "THBFYNJKTGFTTVCGSAE2",
"ZTITLE": "iPad User Guide",
"ZAUTHOR": "Apple Inc.",
"ZGENRE": "Technology",
"ZLANGUAGE": "EN",
"ZLASTOPENDATE": 726602576.413094,
"ZCOVERURL": null
},{
"ZCOVERURL": ''
}, {
"ZASSETID": "THBFYNJKTGFTTVCGSAE3",
"ZTITLE": "Mac User Guide",
"ZAUTHOR": "Apple Inc.",
"ZGENRE": "Technology",
"ZLANGUAGE": "EN",
"ZLASTOPENDATE": 726602576.413094,
"ZCOVERURL": null
},{
"ZCOVERURL": ''
}, {
"ZASSETID": "THBFYNJKTGFTTVCGSAE4",
"ZTITLE": "Apple Watch User Guide",
"ZAUTHOR": "Apple Inc.",
"ZGENRE": "Technology",
"ZLANGUAGE": "EN",
"ZLASTOPENDATE": 726602576.413094,
"ZCOVERURL": null
"ZCOVERURL": ''
}];

export const defaultAnnotations = [{
Expand All @@ -52,7 +52,7 @@ export const defaultAnnotations = [{
"ZANNOTATIONMODIFICATIONDATE": 685151385.91602,
"ZANNOTATIONSTYLE": 3,
"ZANNOTATIONDELETED": 0
},{
}, {
"ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE3",
"ZFUTUREPROOFING5": "Introduction",
"ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the hightlight from the Mac User Guide",
Expand All @@ -62,7 +62,7 @@ export const defaultAnnotations = [{
"ZANNOTATIONMODIFICATIONDATE": 685151385.91602,
"ZANNOTATIONSTYLE": 3,
"ZANNOTATIONDELETED": 0
},{
}, {
"ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE4",
"ZFUTUREPROOFING5": "Introduction",
"ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the hightlight from the Apple Watch User Guide",
Expand Down Expand Up @@ -92,7 +92,7 @@ export const defaultAnnotations = [{
"ZANNOTATIONMODIFICATIONDATE": 685151385.91602,
"ZANNOTATIONSTYLE": 3,
"ZANNOTATIONDELETED": 1
},{
}, {
"ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE3",
"ZFUTUREPROOFING5": "Introduction",
"ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the hightlight from the Mac User Guide",
Expand All @@ -102,7 +102,7 @@ export const defaultAnnotations = [{
"ZANNOTATIONMODIFICATIONDATE": 685151385.91602,
"ZANNOTATIONSTYLE": 3,
"ZANNOTATIONDELETED": 1
},{
}, {
"ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE4",
"ZFUTUREPROOFING5": "Introduction",
"ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the hightlight from the Apple Watch User Guide",
Expand Down
45 changes: 45 additions & 0 deletions src/methods/aggregateDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IBook, IBookAnnotation, ICombinedBooksAndHighlights } from '../types';
import { getBooks } from './getBooks';
import { getAnnotations } from './getAnnotations';

export const aggregateBookAndHighlightDetails = async (): Promise<ICombinedBooksAndHighlights[]> => {
const books = await getBooks();
const annotations = await getAnnotations();

const resultingHighlights: ICombinedBooksAndHighlights[] = books.reduce((highlights: ICombinedBooksAndHighlights[], book: IBook) => {
const bookRelatedAnnotations: IBookAnnotation[] = annotations.filter((annotation: IBookAnnotation) => annotation.ZANNOTATIONASSETID === book.ZASSETID);

if (bookRelatedAnnotations.length > 0) {
// Obsidian forbids adding certain characters to the title of a note, so they must be replaced with a dash (-)
// | # ^ [] \ / :
// after the replacement, two or more spaces are replaced with a single one
// eslint-disable-next-line
const normalizedBookTitle = book.ZTITLE.replace(/[\|\#\^\[\]\\\/\:]+/g, ' -').replace(/\s{2,}/g, ' ');

highlights.push({
bookTitle: normalizedBookTitle,
bookId: book.ZASSETID,
bookAuthor: book.ZAUTHOR,
bookGenre: book.ZGENRE,
bookLanguage: book.ZLANGUAGE,
bookLastOpenedDate: book.ZLASTOPENDATE,
bookCoverUrl: book.ZCOVERURL,
annotations: bookRelatedAnnotations.map(annotation => {
return {
chapter: annotation.ZFUTUREPROOFING5,
contextualText: annotation.ZANNOTATIONREPRESENTATIVETEXT,
highlight: annotation.ZANNOTATIONSELECTEDTEXT,
note: annotation.ZANNOTATIONNOTE,
highlightStyle: annotation.ZANNOTATIONSTYLE,
highlightCreationDate: annotation.ZANNOTATIONCREATIONDATE,
highlightModificationDate: annotation.ZANNOTATIONMODIFICATIONDATE
}
})
})
}

return highlights;
}, []);

return resultingHighlights;
};
13 changes: 13 additions & 0 deletions src/methods/getAnnotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IBookAnnotation } from '../types';
import { annotationsRequest } from '../db';
import { HIGHLIGHTS_DB_PATH, HIGHLIGHTS_LIBRARY_COLUMNS, HIGHLIGHTS_LIBRARY_NAME } from '../db/constants';


export const getAnnotations = async(): Promise<IBookAnnotation[]> => {
const annotationDetails = await annotationsRequest(
HIGHLIGHTS_DB_PATH,
`SELECT ${HIGHLIGHTS_LIBRARY_COLUMNS.join(', ')} FROM ${HIGHLIGHTS_LIBRARY_NAME} WHERE ZANNOTATIONDELETED IS 0 AND ZANNOTATIONSELECTEDTEXT IS NOT NULL`
) as IBookAnnotation[];

return annotationDetails;
}
12 changes: 12 additions & 0 deletions src/methods/getBooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IBook } from '../types';
import { dbRequest } from '../db';
import { BOOKS_DB_PATH, BOOKS_LIBRARY_COLUMNS, BOOKS_LIBRARY_NAME } from '../db/constants';

export const getBooks = async (): Promise<IBook[]> => {
const bookDetails = await dbRequest(
BOOKS_DB_PATH,
`SELECT ${BOOKS_LIBRARY_COLUMNS.join(', ')} FROM ${BOOKS_LIBRARY_NAME} WHERE ZPURCHASEDATE IS NOT NULL`
) as IBook[];

return bookDetails;
}
20 changes: 20 additions & 0 deletions src/methods/renderHighlightsTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import dayjs from 'dayjs';
import * as Handlebars from "handlebars";
import { ICombinedBooksAndHighlights } from '../types';
export const renderHighlightsTemplate = async (highlight: ICombinedBooksAndHighlights, template: string) => {
// TODO: Consider moving to a separate file if there are several helpers to be added
Handlebars.registerHelper("eq", function(a, b) {
if (a == b) {
return this;
}
});

Handlebars.registerHelper("dateFormat", (date, format) => {
return dayjs("2001-01-01").add(date, "s").format(format);
});

const compiledTemplate = Handlebars.compile(template);
const renderedTemplate = compiledTemplate(highlight);

return renderedTemplate;
}
11 changes: 5 additions & 6 deletions src/search.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { App, Notice, SuggestModal } from 'obsidian';
import IBookHighlightsPlugin from '../main';
import { ICombinedBooksAndHighlights } from './types';

import { aggregateBookAndHighlightDetails } from './methods/aggregateDetails';
abstract class IBookHighlightsPluginSuggestModal extends SuggestModal<ICombinedBooksAndHighlights> {
plugin: IBookHighlightsPlugin;
constructor(
app: App,
plugin: IBookHighlightsPlugin) {
super(app);
this.plugin = plugin;
}
}
}

export class IBookHighlightsPluginSearchModal extends IBookHighlightsPluginSuggestModal {
async getSuggestions(query: string): Promise<ICombinedBooksAndHighlights[] > {
try {
const allBooks = await this.plugin.aggregateBookAndHighlightDetails();
console.log('allbooks', allBooks);

const allBooks = await aggregateBookAndHighlightDetails();

return allBooks.filter(book => {
const titleMatch = book.bookTitle.toLowerCase().includes(query.toLowerCase());
const authorMatch = book.bookAuthor.toLowerCase().includes(query.toLowerCase());

return titleMatch || authorMatch;
});
} catch (error) {
Expand Down
File renamed without changes.
25 changes: 25 additions & 0 deletions test/aggregateDetails.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, test, vi } from 'vitest';
import { aggregateBookAndHighlightDetails } from '../src/methods/aggregateDetails';
import * as db from '../src/db';
import { booksToAggregate, annotationsToAggregate, aggregatedHighlights } from './mocks/aggregatedDetailsData';
import { IBook, IBookAnnotation } from '../src/types';

describe('aggregateBookAndHighlightDetails', () => {
test('Should return an array of aggregated highlights when a book has highlights', async () => {
vi.spyOn(db, 'dbRequest').mockResolvedValue(booksToAggregate as IBook[]);
vi.spyOn(db, 'annotationsRequest').mockResolvedValue(annotationsToAggregate as IBookAnnotation[]);

const books = await aggregateBookAndHighlightDetails();

expect(books).toEqual(aggregatedHighlights);
});

test('Should return an empty array when a book has no highlights', async () => {
vi.spyOn(db, 'dbRequest').mockResolvedValue(booksToAggregate as IBook[]);
vi.spyOn(db, 'annotationsRequest').mockResolvedValue([]);

const books = await aggregateBookAndHighlightDetails();

expect(books).toEqual([]);
});
});
Loading

0 comments on commit 2e1d747

Please sign in to comment.