From 2e1d7479abf5294f0cfe7c905d823ad80992fe0f Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 26 Apr 2024 21:50:09 +0300 Subject: [PATCH] test: add tests for main functions. Decouple functions for better testability --- main.ts | 103 +++--------------------- src/db/seedData.ts | 20 ++--- src/methods/aggregateDetails.ts | 45 +++++++++++ src/methods/getAnnotations.ts | 13 +++ src/methods/getBooks.ts | 12 +++ src/methods/renderHighlightsTemplate.ts | 20 +++++ src/search.ts | 11 ++- src/{types.ts => types/index.ts} | 0 test/aggregateDetails.spec.ts | 25 ++++++ test/db.spec.ts | 38 ++++----- test/getAnnotations.spec.ts | 15 ++++ test/getBooks.spec.ts | 15 ++++ test/mocks/aggregatedDetailsData.ts | 40 +++++++++ test/mocks/rawTemplates.ts | 31 +++++++ test/mocks/renderedTemplate.ts | 40 +++++++++ test/renderHighlightsTemplate.spec.ts | 44 ++++++++++ tsconfig.json | 2 +- 17 files changed, 345 insertions(+), 129 deletions(-) create mode 100644 src/methods/aggregateDetails.ts create mode 100644 src/methods/getAnnotations.ts create mode 100644 src/methods/getBooks.ts create mode 100644 src/methods/renderHighlightsTemplate.ts rename src/{types.ts => types/index.ts} (100%) create mode 100644 test/aggregateDetails.spec.ts create mode 100644 test/getAnnotations.spec.ts create mode 100644 test/getBooks.spec.ts create mode 100644 test/mocks/aggregatedDetailsData.ts create mode 100644 test/mocks/rawTemplates.ts create mode 100644 test/mocks/renderedTemplate.ts create mode 100644 test/renderHighlightsTemplate.spec.ts diff --git a/main.ts b/main.ts index 17780d1..05939d8 100644 --- a/main.ts +++ b/main.ts @@ -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; @@ -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', @@ -56,7 +43,7 @@ export default class IBookHighlightsPlugin extends Plugin { } }, }); - + this.addCommand({ id: 'import-single-highlights', name: 'From a specific book...', @@ -84,68 +71,9 @@ export default class IBookHighlightsPlugin extends Plugin { await this.saveData(this.settings); } - async getBooks(): Promise { - 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 { - 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 { - 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 { - 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.'); } @@ -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`)), diff --git a/src/db/seedData.ts b/src/db/seedData.ts index 2c3232f..90d3085 100644 --- a/src/db/seedData.ts +++ b/src/db/seedData.ts @@ -5,7 +5,7 @@ export const defaultBooks = [{ "ZGENRE": "Technology", "ZLANGUAGE": "EN", "ZLASTOPENDATE": 726602576.413094, - "ZCOVERURL": null + "ZCOVERURL": '' }, { "ZASSETID": "THBFYNJKTGFTTVCGSAE2", "ZTITLE": "iPad User Guide", @@ -13,23 +13,23 @@ export const defaultBooks = [{ "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 = [{ @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/src/methods/aggregateDetails.ts b/src/methods/aggregateDetails.ts new file mode 100644 index 0000000..63a994a --- /dev/null +++ b/src/methods/aggregateDetails.ts @@ -0,0 +1,45 @@ +import { IBook, IBookAnnotation, ICombinedBooksAndHighlights } from '../types'; +import { getBooks } from './getBooks'; +import { getAnnotations } from './getAnnotations'; + +export const aggregateBookAndHighlightDetails = async (): Promise => { + 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; +}; diff --git a/src/methods/getAnnotations.ts b/src/methods/getAnnotations.ts new file mode 100644 index 0000000..fb1bf57 --- /dev/null +++ b/src/methods/getAnnotations.ts @@ -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 => { + 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; +} diff --git a/src/methods/getBooks.ts b/src/methods/getBooks.ts new file mode 100644 index 0000000..c79b082 --- /dev/null +++ b/src/methods/getBooks.ts @@ -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 => { + 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; +} diff --git a/src/methods/renderHighlightsTemplate.ts b/src/methods/renderHighlightsTemplate.ts new file mode 100644 index 0000000..df1672a --- /dev/null +++ b/src/methods/renderHighlightsTemplate.ts @@ -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; +} diff --git a/src/search.ts b/src/search.ts index 3d41235..c9376cc 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,7 +1,7 @@ import { App, Notice, SuggestModal } from 'obsidian'; import IBookHighlightsPlugin from '../main'; import { ICombinedBooksAndHighlights } from './types'; - +import { aggregateBookAndHighlightDetails } from './methods/aggregateDetails'; abstract class IBookHighlightsPluginSuggestModal extends SuggestModal { plugin: IBookHighlightsPlugin; constructor( @@ -9,19 +9,18 @@ abstract class IBookHighlightsPluginSuggestModal extends SuggestModal { 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) { diff --git a/src/types.ts b/src/types/index.ts similarity index 100% rename from src/types.ts rename to src/types/index.ts diff --git a/test/aggregateDetails.spec.ts b/test/aggregateDetails.spec.ts new file mode 100644 index 0000000..f0c9ab0 --- /dev/null +++ b/test/aggregateDetails.spec.ts @@ -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([]); + }); +}); diff --git a/test/db.spec.ts b/test/db.spec.ts index 62c7dc0..49932ce 100644 --- a/test/db.spec.ts +++ b/test/db.spec.ts @@ -11,10 +11,10 @@ import { defaultBooks, defaultAnnotations } from '../src/db/seedData'; afterEach(async () => { let dbPath = path.join(process.cwd(), TEST_DATABASE_PATH); - + const sqlite = new Database(dbPath); const db = drizzle(sqlite); - + await db.delete(bookLibrary); await db.delete(annotations); }); @@ -23,19 +23,19 @@ describe('Empty database', () => { test('Should throw an error when books library is empty', async () => { try { let dbPath = path.join(process.cwd(), TEST_DATABASE_PATH); - + await dbRequest(dbPath, `SELECT * FROM ${BOOKS_LIBRARY_NAME}`); - } catch (error) { + } catch (error) { expect(error).toEqual('No books found. Looks like your Apple Books library is empty.'); } }); - + test('Should throw an error when no highlights found', async () => { try { await seedDatabase(bookLibrary, defaultBooks); - + let dbPath = path.join(process.cwd(), TEST_DATABASE_PATH); - + await annotationsRequest(dbPath, `SELECT * FROM ${HIGHLIGHTS_LIBRARY_NAME}`); } catch (error) { expect(error).toEqual(`No highlights found. Make sure you made some highlights in your Apple Books.`); @@ -46,19 +46,19 @@ describe('Empty database', () => { describe('Database operations', () => { test('Should return a list of books when books library is not empty', async () => { await seedDatabase(bookLibrary, defaultBooks); - + let dbPath = path.join(process.cwd(), TEST_DATABASE_PATH); const books = await dbRequest(dbPath, `SELECT * FROM ${BOOKS_LIBRARY_NAME}`); - + expect(books).toEqual(defaultBooks); }); - + test('Should return a list of highlights when highlights library is not empty', async () => { await seedDatabase(annotations, defaultAnnotations); - + let dbPath = path.join(process.cwd(), TEST_DATABASE_PATH); const highlights = await annotationsRequest(dbPath, `SELECT * FROM ${HIGHLIGHTS_LIBRARY_NAME} WHERE ZANNOTATIONDELETED = 0`); - + expect(highlights.length).toEqual(4); expect(highlights[0].ZANNOTATIONNOTE).toEqual('Test note for the hightlight from the iPhone User Guide'); expect(highlights[3].ZANNOTATIONREPRESENTATIVETEXT).toEqual('This is a contextual text for the hightlight from the Apple Watch User Guide'); @@ -67,10 +67,10 @@ describe('Database operations', () => { describe('Database load testing', () => { test('Should return 1000 books and in less than 500ms', async () => { - + let oneThousandBooks = []; let threeThousandsAnnotations = []; - + // create 1000 books and 3 annotations for each book for (let i = 0; i < 1000; i++) { oneThousandBooks.push({ @@ -82,7 +82,7 @@ describe('Database load testing', () => { ZLASTOPENDATE: 685151385.91602, ZCOVERURL: null }); - + for (let j = 0; j < 3; j++) { threeThousandsAnnotations.push({ ZANNOTATIONASSETID: `THBFYNJKTGFTTVCGSAE${i}`, @@ -97,19 +97,19 @@ describe('Database load testing', () => { }); } } - + let dbPath = path.join(process.cwd(), TEST_DATABASE_PATH); await seedDatabase(bookLibrary, oneThousandBooks); await seedDatabase(annotations, threeThousandsAnnotations); - + const startTime = Date.now(); const dbBooks = await dbRequest(dbPath, `SELECT * FROM ${BOOKS_LIBRARY_NAME}`); const dbAnnotations = await annotationsRequest(dbPath, `SELECT * FROM ${HIGHLIGHTS_LIBRARY_NAME} WHERE ZANNOTATIONDELETED = 0`); const endTime = Date.now(); - + expect(dbBooks.length).toEqual(1000); expect(dbAnnotations.length).toEqual(3000); - + expect(endTime - startTime).toBeLessThan(500); }); }); diff --git a/test/getAnnotations.spec.ts b/test/getAnnotations.spec.ts new file mode 100644 index 0000000..bfcdeeb --- /dev/null +++ b/test/getAnnotations.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getAnnotations } from '../src/methods/getAnnotations'; +import * as db from '../src/db'; +import { defaultAnnotations } from '../src/db/seedData'; +import { IBookAnnotation } from '../src/types'; + +describe('getAnnotations', () => { + test('Should return an array of annotations', async () => { + vi.spyOn(db, 'annotationsRequest').mockResolvedValue(defaultAnnotations as IBookAnnotation[]); + + const books = await getAnnotations(); + + expect(books).toEqual(defaultAnnotations); + }); +}); diff --git a/test/getBooks.spec.ts b/test/getBooks.spec.ts new file mode 100644 index 0000000..44c7138 --- /dev/null +++ b/test/getBooks.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getBooks } from '../src/methods/getBooks'; +import * as db from '../src/db'; +import { defaultBooks } from '../src/db/seedData'; +import { IBook } from '../src/types'; + +describe('getBooks', () => { + test('Should return an array of books', async () => { + vi.spyOn(db, 'dbRequest').mockResolvedValue(defaultBooks as IBook[]); + + const books = await getBooks(); + + expect(books).toEqual(defaultBooks); + }); +}); diff --git a/test/mocks/aggregatedDetailsData.ts b/test/mocks/aggregatedDetailsData.ts new file mode 100644 index 0000000..56e8033 --- /dev/null +++ b/test/mocks/aggregatedDetailsData.ts @@ -0,0 +1,40 @@ +export const booksToAggregate = [{ + "ZASSETID": "THBFYNJKTGFTTVCGSAE5", + "ZTITLE": "Apple iPhone: User Guide | Instructions ^ with # restricted [ symbols ] in \ / title", + "ZAUTHOR": "Apple Inc.", + "ZGENRE": "Technology", + "ZLANGUAGE": "EN", + "ZLASTOPENDATE": 731876693.002279, + "ZCOVERURL": '' +}]; + +export const annotationsToAggregate = [{ + "ZANNOTATIONASSETID": "THBFYNJKTGFTTVCGSAE5", + "ZFUTUREPROOFING5": "Aggregated Introduction", + "ZANNOTATIONREPRESENTATIVETEXT": "This is a contextual text for the aggregated hightlight from the Apple iPhone User Guide", + "ZANNOTATIONSELECTEDTEXT": "aggregated hightlight from the Apple iPhone User Guide", + "ZANNOTATIONNOTE": "Test note for the aggregated hightlight from the Apple iPhone User Guide", + "ZANNOTATIONCREATIONDATE": 731876693.002279, + "ZANNOTATIONMODIFICATIONDATE": 731876693.002279, + "ZANNOTATIONSTYLE": 3, + "ZANNOTATIONDELETED": 0 +}]; + +export const aggregatedHighlights = [{ + "bookTitle": "Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title", + "bookId": "THBFYNJKTGFTTVCGSAE5", + "bookAuthor": "Apple Inc.", + "bookGenre": "Technology", + "bookLanguage": "EN", + "bookLastOpenedDate": 731876693.002279, + "bookCoverUrl": '', + "annotations": [{ + "chapter": "Aggregated Introduction", + "contextualText": "This is a contextual text for the aggregated hightlight from the Apple iPhone User Guide", + "highlight": "aggregated hightlight from the Apple iPhone User Guide", + "note": "Test note for the aggregated hightlight from the Apple iPhone User Guide", + "highlightStyle": 3, + "highlightCreationDate": 731876693.002279, + "highlightModificationDate": 731876693.002279 + }] +}]; diff --git a/test/mocks/rawTemplates.ts b/test/mocks/rawTemplates.ts new file mode 100644 index 0000000..341b3b3 --- /dev/null +++ b/test/mocks/rawTemplates.ts @@ -0,0 +1,31 @@ +export const rawCustomTemplate = `Title:: 📕 {{{bookTitle}}} +Author:: {{{bookAuthor}}} +Genre:: {{#if bookGenre}}{{{bookGenre}}}{{else}}N/A{{/if}} +Language:: {{#if bookLanguage}}{{bookLanguage}}{{else}}N/A{{/if}} +Last Read:: {{dateFormat bookLastOpenedDate "YYYY-MM-DD hh:mm:ss A Z"}} +Link:: [Apple Books Link](ibooks://assetid/{{bookId}}) + +{{#if bookCoverUrl}}![Book Cover]({{{bookCoverUrl}}}){{/if}} + +## Annotations + +Number of annotations:: {{annotations.length}} + +{{#each annotations}} +---- + +- 📖 Chapter:: {{#if chapter}}{{{chapter}}}{{else}}N/A{{/if}} +- 🔖 Context:: {{#if contextualText}}{{{contextualText}}}{{else}}N/A{{/if}} +{{#if (eq highlightStyle "0")}}- 🎯 Highlight:: {{{highlight}}} +{{else if (eq highlightStyle "1")}}- 🎯 Highlight:: {{{highlight}}} +{{else if (eq highlightStyle "2")}}- 🎯 Highlight:: {{{highlight}}} +{{else if (eq highlightStyle "3")}}- 🎯 Highlight:: {{{highlight}}} +{{else if (eq highlightStyle "4")}}- 🎯 Highlight:: {{{highlight}}} +{{else if (eq highlightStyle "5")}}- 🎯 Highlight:: {{{highlight}}} +{{/if}} +- 📝 Note:: {{#if note}}{{{note}}}{{else}}N/A{{/if}} +- 📅 Highlight taken on:: {{dateFormat highlightCreationDate "YYYY-MM-DD hh:mm:ss A Z"}} +- 📅 Highlight modified on:: {{dateFormat highlightModificationDate "YYYY-MM-DD hh:mm:ss A Z"}} + +{{/each}} +`; diff --git a/test/mocks/renderedTemplate.ts b/test/mocks/renderedTemplate.ts new file mode 100644 index 0000000..da79ac2 --- /dev/null +++ b/test/mocks/renderedTemplate.ts @@ -0,0 +1,40 @@ +export const defaultTemplateMock = `Title:: 📕 Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title +Author:: Apple Inc. +Link:: [Apple Books Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5) + +## Annotations + +Number of annotations:: 1 + +---- + +- 📖 Chapter:: Aggregated Introduction +- 🔖 Context:: This is a contextual text for the aggregated hightlight from the Apple iPhone User Guide +- 🎯 Highlight:: aggregated hightlight from the Apple iPhone User Guide +- 📝 Note:: Test note for the aggregated hightlight from the Apple iPhone User Guide + +`; + +export const renderedCustomTemplateMock = `Title:: 📕 Apple iPhone - User Guide - Instructions - with - restricted - symbols - in - title +Author:: Apple Inc. +Genre:: Technology +Language:: EN +Last Read:: 2024-03-11 07:04:53 PM +02:00 +Link:: [Apple Books Link](ibooks://assetid/THBFYNJKTGFTTVCGSAE5) + + + +## Annotations + +Number of annotations:: 1 + +---- + +- 📖 Chapter:: Aggregated Introduction +- 🔖 Context:: This is a contextual text for the aggregated hightlight from the Apple iPhone User Guide +- 🎯 Highlight:: aggregated hightlight from the Apple iPhone User Guide +- 📝 Note:: Test note for the aggregated hightlight from the Apple iPhone User Guide +- 📅 Highlight taken on:: 2024-03-11 07:04:53 PM +02:00 +- 📅 Highlight modified on:: 2024-03-11 07:04:53 PM +02:00 + +`; diff --git a/test/renderHighlightsTemplate.spec.ts b/test/renderHighlightsTemplate.spec.ts new file mode 100644 index 0000000..6d26244 --- /dev/null +++ b/test/renderHighlightsTemplate.spec.ts @@ -0,0 +1,44 @@ +import Handlebars from 'handlebars'; +import { describe, expect, test } from 'vitest'; +import { renderHighlightsTemplate } from '../src/methods/renderHighlightsTemplate'; +import { aggregatedHighlights } from './mocks/aggregatedDetailsData'; +import { rawCustomTemplate } from './mocks/rawTemplates'; +import { defaultTemplateMock, renderedCustomTemplateMock } from './mocks/renderedTemplate'; +import defaultTemplate from '../src/template'; + +describe('renderHighlightsTemplate', () => { + test('Should render a default template with the provided data', async () => { + const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlights[0], defaultTemplate); + + expect(renderedTemplate).toEqual(defaultTemplateMock); + }); + + test('Should render a custom template with the provided data', async () => { + const renderedTemplate = await renderHighlightsTemplate(aggregatedHighlights[0], rawCustomTemplate); + + expect(renderedTemplate).toEqual(renderedCustomTemplateMock); + }); +}); + +describe('Custom Handlebars helpers', () => { + let helpers = Handlebars.helpers; + + describe('eq', () => { + test('Should properly compare two values', async () => { + expect(helpers.eq).toBeDefined(); + + expect(helpers.eq(1, '1')).toBeTruthy(); + expect(helpers.eq(1, '2')).toBeFalsy(); + }); + }); + + describe('dateFormat', () => { + test('Should properly calculate and format a date', async () => { + expect(helpers.dateFormat).toBeDefined(); + + expect(helpers.dateFormat(683246482.042544, 'YYYY-MM-DD hh:mm:ss A Z')).toEqual('2022-08-26 11:41:22 PM +03:00'); + expect(helpers.dateFormat(726187452.01369, "ddd, MMM DD YYYY, HH:mm:ss Z")).toEqual("Fri, Jan 05 2024, 22:44:12 +02:00"); + expect(helpers.dateFormat(731876693.002279, 'dddd, MMMM D, YYYY [at] hh:mm A')).toEqual('Monday, March 11, 2024 at 07:04 PM'); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 3eea23a..b904126 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,4 +23,4 @@ "include": [ "**/*.ts" ] -} \ No newline at end of file +}