diff --git a/package.json b/package.json index bc7982a6..14b5405e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "js": true, "ts": "module" }, + "timeout": "30s", "files": [ "src/**/*.test.*" ], diff --git a/src/view-model/navigation/top-bar-model.test.ts b/src/view-model/navigation/top-bar-model.test.ts index 1a1b2c22..242bd4ce 100644 --- a/src/view-model/navigation/top-bar-model.test.ts +++ b/src/view-model/navigation/top-bar-model.test.ts @@ -1,7 +1,13 @@ -import test from 'ava' -import { ScrollViewModel } from '../scroll-view-model.ts' +import test, { ExecutionContext } from 'ava' +import { + RenderedEntry, + RenderedLineInfo, + ScrollViewModel, +} from '../scroll-view-model.ts' import { LeiningGenerator } from '../../calendar-model/generator.ts' import { UserSettings } from '../../calendar-model/user-settings.ts' +import { fetchPages, renderLine } from '../test-utils.ts' +import { Link, TopBarTracker } from './top-bar-model.ts' const testSettings: UserSettings = { ashkenazi: true, @@ -11,11 +17,138 @@ const testSettings: UserSettings = { const generator = new LeiningGenerator(testSettings) -test('renders for בראשית', (t) => { - const viewModel = ScrollViewModel.forId( - generator, - '2024-10-26:shacharis,main' +const cachedTracker = new TopBarTracker() + +let viewModel: ScrollViewModel | null = null +let pages: RenderedEntry[] = [] + +async function createModel( + id: string, + fetch: Parameters[1] +) { + viewModel = ScrollViewModel.forId(generator, id) + pages = await fetchPages(viewModel, fetch) +} + +test.afterEach('reset state', () => { + viewModel = null + pages = [] +}) + +test.serial('renders for the beginning of בראשית', async (t) => { + await createModel('2024-10-26:shacharis,main', { + count: 5, + fetchPreviousPages: false, + }) + + t.snapshot( + renderResult(t, { + first: firstLine(pages[0]), + center: null, + last: firstLine(pages[1]), + }) + ) +}) + +test.serial('renders for שמחת תורה', async (t) => { + await createModel('2024-10-25:shacharis,main', { + count: 5, + fetchPreviousPages: false, + }) + + t.snapshot( + renderResult(t, { + first: firstLine(pages[0]), + center: null, + last: firstLine(pages[1]), + }) + ) +}) + +test.serial('שקלים / ראש חודש as פרשה', async (t) => { + await createModel('2025-03-01:shacharis,main', { + count: 5, + fetchPreviousPages: false, + }) + + t.snapshot( + renderResult(t, { + first: getLine( + 'וַיְדַבֵּ֥ר יְהֹוָ֖ה אֶל־מֹשֶׁ֥ה לֵּאמֹֽר׃ דַּבֵּר֙ אֶל־בְּנֵ֣י יִשְׂרָאֵ֔ל' + ), + center: null, + last: getLine( + 'זָהָ֣ב טָה֑וֹר אַמָּתַ֤יִם וָחֵ֙צִי֙ אׇרְכָּ֔הּ וְאַמָּ֥ה וָחֵ֖צִי רׇחְבָּֽהּ׃' + ), + }) ) - // TODO: Snapshot - t.is(viewModel, viewModel) }) + +test.serial('renders for חול המועד סוכות', async (t) => { + await createModel('2024-10-22:shacharis,main', { + count: 5, + fetchPreviousPages: false, + }) + + t.snapshot( + renderResult(t, { + first: getLine( + 'הַחֲמִישִׁ֛י פָּרִ֥ים תִּשְׁעָ֖ה אֵילִ֣ם שְׁנָ֑יִם כְּבָשִׂ֧ים בְּנֵֽי' + ), + center: getLine( + 'שְׁמֹנָ֖ה אֵילִ֣ם שְׁנָ֑יִם כְּבָשִׂ֧ים בְּנֵי־שָׁנָ֛ה אַרְבָּעָ֥ה עָשָׂ֖ר' + ), + last: getLine( + 'וּשְׂעִ֥יר חַטָּ֖את אֶחָ֑ד מִלְּבַד֙ עֹלַ֣ת הַתָּמִ֔יד מִנְחָתָ֖הּ' + ), + }) + ) +}) + +function renderResult( + t: ExecutionContext, + lines: Parameters[1] +) { + if (!viewModel) throw new Error('Must create viewModel first') + const cachedResult = cachedTracker.setLine(viewModel, lines) + const freshResult = new TopBarTracker().setLine(viewModel, lines) + t.deepEqual(cachedResult, freshResult) + return { + aliyahRange: cachedResult.aliyahRange, + currentRun: cachedResult.currentRun?.id, + previousLink: renderLink(cachedResult.previousLink), + nextLink: renderLink(cachedResult.nextLink), + relatedRuns: cachedResult.relatedRuns.map(renderLink), + } +} + +function renderLink(link: Link | null) { + if (!link) return link + return { + targetRun: link.targetRun.id, + label: link.label, + } +} + +function firstLine(page: RenderedEntry) { + if (page.type !== 'page') throw new Error('Must be a page') + return page.lines[0] +} + +/** Finds the single line containing the specified text. */ +function getLine(text: string): RenderedLineInfo { + if (!viewModel) throw new Error('Must create viewModel first') + const results = pages.flatMap((p) => + p.type === 'message' + ? [] + : p.lines.filter((line) => renderLine(line).includes(text)) + ) + + if (results.length !== 1) + throw new Error( + `Found ${results.length} lines: + +${results.map(renderLine).join('\n')}`.trim() + ) + return results[0] +} diff --git a/src/view-model/navigation/top-bar-model.test.ts.md b/src/view-model/navigation/top-bar-model.test.ts.md new file mode 100644 index 00000000..7455ee0d --- /dev/null +++ b/src/view-model/navigation/top-bar-model.test.ts.md @@ -0,0 +1,135 @@ +# Snapshot report for `src/view-model/navigation/top-bar-model.test.ts` + +The actual snapshot is saved in `top-bar-model.test.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## renders for the beginning of בראשית + +> Snapshot 1 + + { + aliyahRange: [ + 'ראשון', + ], + currentRun: '2024-10-26:shacharis,main', + nextLink: { + label: 'ראש חודש חשון שחרית', + targetRun: '2024-11-01:shacharis,main', + }, + previousLink: { + label: 'שמחת תורה שחרית', + targetRun: '2024-10-25:shacharis,main', + }, + relatedRuns: [ + { + label: 'פרשת בראשית', + targetRun: '2024-10-26:shacharis,main', + }, + { + label: 'הפטרה', + targetRun: '2024-10-26:shacharis,haftara', + }, + ], + } + +## renders for שמחת תורה + +> Snapshot 1 + + { + aliyahRange: [ + 'רביעי', + ], + currentRun: '2024-10-25:shacharis,main', + nextLink: { + label: 'פרשת בראשית שחרית', + targetRun: '2024-10-26:shacharis,main', + }, + previousLink: { + label: 'שמיני עצרת מעריב', + targetRun: '2024-10-24:maariv,main', + }, + relatedRuns: [ + { + label: 'שמחת תורה', + targetRun: '2024-10-25:shacharis,main', + }, + { + label: 'חתן בראשית', + targetRun: '2024-10-25:shacharis,last-aliyah', + }, + { + label: 'מפטיר', + targetRun: '2024-10-25:shacharis,maftir', + }, + { + label: 'הפטרה', + targetRun: '2024-10-25:shacharis,haftara', + }, + ], + } + +## שקלים / ראש חודש as פרשה + +> Snapshot 1 + + { + aliyahRange: [ + 'ראשון', + 'שני', + ], + currentRun: '2025-03-01:shacharis,main', + nextLink: { + label: 'פרשת תצוה שחרית', + targetRun: '2025-03-08:shacharis,main', + }, + previousLink: { + label: 'ראש חודש אדר שחרית', + targetRun: '2025-02-28:shacharis,main', + }, + relatedRuns: [ + { + label: 'פרשת תרומה', + targetRun: '2025-03-01:shacharis,main', + }, + { + label: 'שביעי', + targetRun: '2025-03-01:shacharis,last-aliyah', + }, + { + label: 'מפטיר', + targetRun: '2025-03-01:shacharis,maftir', + }, + { + label: 'הפטרה', + targetRun: '2025-03-01:shacharis,haftara', + }, + ], + } + +## renders for חול המועד סוכות + +> Snapshot 1 + + { + aliyahRange: [ + 'ראשון', + 'שלישי', + ], + currentRun: '2024-10-22:shacharis,main', + nextLink: { + label: 'סוכות ז׳ (הושענא רבה) שחרית', + targetRun: '2024-10-23:shacharis,main', + }, + previousLink: { + label: 'סוכות חול המועד יום ג׳ שחרית', + targetRun: '2024-10-21:shacharis,main', + }, + relatedRuns: [ + { + label: 'סוכות חול המועד יום ד׳', + targetRun: '2024-10-22:shacharis,main', + }, + ], + } diff --git a/src/view-model/navigation/top-bar-model.test.ts.snap b/src/view-model/navigation/top-bar-model.test.ts.snap new file mode 100644 index 00000000..2e865bea Binary files /dev/null and b/src/view-model/navigation/top-bar-model.test.ts.snap differ diff --git a/src/view-model/navigation/top-bar-model.ts b/src/view-model/navigation/top-bar-model.ts index e091c199..d06bf529 100644 --- a/src/view-model/navigation/top-bar-model.ts +++ b/src/view-model/navigation/top-bar-model.ts @@ -2,9 +2,9 @@ import { LeiningAliyah, LeiningInstance, LeiningRun, -} from '../../calendar-model/model-types' -import { aliyahName } from '../aliyah-labeller' -import { RenderedLineInfo, ScrollViewModel } from '../scroll-view-model' +} from '../../calendar-model/model-types.ts' +import { aliyahName } from '../aliyah-labeller.ts' +import { RenderedLineInfo, ScrollViewModel } from '../scroll-view-model.ts' /** The information and link targets displayed in the top navigation bar. */ export interface TopBarInfo { @@ -48,7 +48,7 @@ export interface TopBarInfo { relatedRuns: Link[] } -interface Link { +export interface Link { targetRun: LeiningRun /** Will omit context if the same as the current run. */ label: string @@ -98,7 +98,7 @@ export class TopBarTracker { const lastAliyah = lines.last?.aliyot[0] ?? lines.center?.aliyot[0] const run = lines.center?.run ?? lines.last?.run ?? lines.first?.run - if (!run) return + if (!run) return this.currentInfo newInfo.currentRun = run if ( @@ -108,7 +108,7 @@ export class TopBarTracker { newInfo.relatedRuns = newInfo.currentRun.leining.runs.map((run) => ({ // Use aliyahName() to turn שביעי into חתן בראשית. // Use `run.type` to label הפטרות. - label: aliyahName(run.aliyot[0].index, run) ?? run.type, + label: aliyahName(run.aliyot[0].index, run) || run.type, targetRun: run, })) @@ -157,6 +157,7 @@ export class TopBarTracker { } this.currentInfo = newInfo + return this.currentInfo } private generateAliyahRange(currentRun: LeiningRun): string[] { @@ -165,7 +166,7 @@ export class TopBarTracker { ? [this.firstAliyah] : [this.firstAliyah, this.lastAliyah] ) - .filter((a) => a != null) + .filter((a): a is NonNullable => a != null) .map((a) => { // Pass isEnd to label ראשון instead of repeating the leining name. const label = aliyahName(a.aliyah.index, a.run, { isEnd: true }) diff --git a/src/view-model/scroll-view-model.test.ts b/src/view-model/scroll-view-model.test.ts index ea41bfb7..180a4474 100644 --- a/src/view-model/scroll-view-model.test.ts +++ b/src/view-model/scroll-view-model.test.ts @@ -7,7 +7,7 @@ import { ScrollViewModel, } from './scroll-view-model.ts' import { last } from '../calendar-model/utils.ts' -import { renderLine } from './test-utils.ts' +import { fetchPages, renderLine } from './test-utils.ts' import { containsRef } from '../calendar-model/ref-utils.ts' const testSettings: UserSettings = { @@ -307,23 +307,6 @@ function lineContains(line: RenderedLineInfo, range: number[] | undefined) { return line.verses.some(({ c, v }) => c === range[0] && v === range[1]) } -async function fetchPages( - model: ScrollViewModel | null, - { count, fetchPreviousPages }: { count: number; fetchPreviousPages: boolean } -): Promise { - if (!model) throw new Error('No model') - const pages: RenderedEntry[] = [(await model.startingLocation).page] - - if (fetchPreviousPages) count /= 2 - for (let i = 0; i < count; i++) { - const previousPage = await model.fetchPreviousPage() - if (previousPage) pages.unshift(previousPage) - const nextPage = await model.fetchNextPage() - if (nextPage) pages.push(nextPage) - } - return pages -} - /** Renders enough properties of a scroll to make `deepEqual()` work with useful failures. */ async function renderScroll(model: ScrollViewModel | null) { return ( diff --git a/src/view-model/test-utils.ts b/src/view-model/test-utils.ts index 29ccece7..1c3da577 100644 --- a/src/view-model/test-utils.ts +++ b/src/view-model/test-utils.ts @@ -1,7 +1,29 @@ -import { RenderedLineInfo } from './scroll-view-model.ts' +import { + RenderedEntry, + RenderedLineInfo, + ScrollViewModel, +} from './scroll-view-model.ts' export function renderLine(line: RenderedLineInfo): string { return `${line.labels}: ${line.text .map((spans) => spans.join(' ')) .join('\t')}` } + +/** Fetches a range of pages around the start of a ScrollViewModel. */ +export async function fetchPages( + model: ScrollViewModel | null, + { count, fetchPreviousPages }: { count: number; fetchPreviousPages: boolean } +): Promise { + if (!model) throw new Error('No model') + const pages: RenderedEntry[] = [(await model.startingLocation).page] + + if (fetchPreviousPages) count /= 2 + for (let i = 0; i < count; i++) { + const previousPage = await model.fetchPreviousPage() + if (previousPage) pages.unshift(previousPage) + const nextPage = await model.fetchNextPage() + if (nextPage) pages.push(nextPage) + } + return pages +}