Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/desktop/src/main/test-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getWritebackDebugState } from './sync/crdt-writeback'
import { getCrdtQueue, getNetworkMonitor } from './sync/runtime'
import { getDatabase } from './database'
import { sql } from 'drizzle-orm'
import { getNoteMetadataById } from '@memry/storage-data'
import { CalendarChannels, TasksChannels } from '@memry/contracts/ipc-channels'

export interface SyncTestBootstrapInput {
Expand All @@ -27,12 +28,19 @@ export interface CalendarProjectionSeedInput {
snoozeTitle: string
}

export interface NoteOnDeviceStatus {
recordPresent: boolean
crdtPresent: boolean
crdtBody: string | null
}

interface MemryTestHooks {
bootstrapSyncDevice(input: SyncTestBootstrapInput): Promise<{ deviceId: string }>
setNetworkOnlineForTests(online: boolean): Promise<void>
getCrdtPendingCount(): Promise<number>
seedCalendarProjection(input: CalendarProjectionSeedInput): Promise<void>
getCrdtDocMarkdown(noteId: string): Promise<string | null>
hasNoteOnDevice(noteId: string): Promise<NoteOnDeviceStatus>
getWritebackDebugState(noteId: string): Promise<{
pending: boolean
scheduledCount: number
Expand Down Expand Up @@ -279,6 +287,17 @@ export function registerTestHooks(): void {
return yDocToMarkdown(doc)
},

async hasNoteOnDevice(noteId: string): Promise<NoteOnDeviceStatus> {
const record = getNoteMetadataById(getDatabase(), noteId)
const doc = getCrdtProvider().getDoc(noteId)
const crdtBody = doc ? await yDocToMarkdown(doc) : null
return {
recordPresent: record != null,
crdtPresent: doc != null,
crdtBody
}
},

async getWritebackDebugState(noteId: string) {
return getWritebackDebugState(noteId)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export function CalendarWeekView({
className="flex h-full min-h-0 flex-col [--grid-line-color:var(--border)]"
data-testid="calendar-view"
data-view="week"
data-anchor-date={anchorDate}
data-visible-day-start={visibleDayStart}
>
<div className="flex border-b border-border">
<div
Expand Down
83 changes: 11 additions & 72 deletions apps/desktop/tests/e2e/body-crdt-coverage-variants.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
waitForSyncOffline,
waitForSyncOnline
} from './utils/network-control'
import { waitForNoteReplicated } from './utils/wait-helpers'

type CursorPosition = 'start' | 'end'
type ReconnectOrder = 'a-first' | 'b-first' | 'together'
Expand Down Expand Up @@ -273,28 +274,6 @@ async function waitForClosedDeviceNoteConvergence(
await expect.poll(() => getCrdtDocBodyByTitle(page, electronApp, title)).toBe(finalBody)
}

async function readReplicatedNoteStatus(
electronApp: ElectronApplication,
page: Page,
note: NoteHandle,
expectedBody: string
): Promise<{
crdtBody: string | null
fileBody: string | null
ready: boolean
}> {
const [crdtBody, fileBody] = await Promise.all([
getCrdtDocBodyById(electronApp, note.id),
getNoteFileBodyById(page, note.id)
])

return {
crdtBody,
fileBody,
ready: crdtBody === expectedBody && fileBody === expectedBody
}
}

async function runOfflineOfflineMergeCase({
electronAppA,
electronAppB,
Expand Down Expand Up @@ -610,56 +589,16 @@ test.describe('Body CRDT coverage variants', () => {
pageB,
order: 'together'
})
const remoteChecks = [
{
label: 'noteB on A',
electronApp: electronAppA,
page: pageA,
note: noteB,
expectedBody: 'noteB shared merge block'
},
{
label: 'noteA on B',
electronApp: electronAppB,
page: pageB,
note: noteA,
expectedBody: 'noteA shared merge block'
}
] as const

await expect
.poll(
async () => {
await syncBothAndWait(pageA, pageB, 30000)
const statuses = await Promise.all(
remoteChecks.map(async (check) => ({
label: check.label,
...(await readReplicatedNoteStatus(
check.electronApp,
check.page,
check.note,
check.expectedBody
))
}))
)
return statuses
},
{ timeout: 120_000, intervals: [500, 1_000, 2_000, 5_000] }
)
.toEqual([
{
label: 'noteB on A',
crdtBody: 'noteB shared merge block',
fileBody: 'noteB shared merge block',
ready: true
},
{
label: 'noteA on B',
crdtBody: 'noteA shared merge block',
fileBody: 'noteA shared merge block',
ready: true
}
])

await syncBothAndWait(pageA, pageB, 30000)
await Promise.all([
waitForNoteReplicated(electronAppA, noteB.id, 'noteB shared merge block', {
timeout: 90_000
}),
waitForNoteReplicated(electronAppB, noteA.id, 'noteA shared merge block', {
timeout: 90_000
})
])

const noteAOnA = noteA
const noteBOnA = noteB
Expand Down
112 changes: 48 additions & 64 deletions apps/desktop/tests/e2e/calendar-week-scroll.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import type { Page } from '@playwright/test'
import { test, expect } from './fixtures'
import { waitForAppReady, waitForVaultReady } from './utils/electron-helpers'
import { getVisibleDayStart, waitForStable } from './utils/wait-helpers'

const STABLE_FOR_MS = 500
const STABLE_TIMEOUT_MS = 15_000

async function openCalendarWeekView(page: Page): Promise<void> {
await page.getByRole('button', { name: 'Calendar' }).click()
await expect(page.getByTestId('calendar-page')).toBeVisible()
await page.getByTestId('calendar-page').getByRole('button', { name: 'Week', exact: true }).click()
await expect(page.getByTestId('calendar-view')).toHaveAttribute('data-view', 'week')
await expect(page.getByTestId('calendar-week-scroll')).toBeVisible()
}

async function getScrollLeft(page: Page): Promise<number> {
return await page
.getByTestId('calendar-week-scroll')
.evaluate((el) => (el as HTMLElement).scrollLeft)
// Let the virtualizer's initial scroll + scroll listener settle so
// visibleDayStart reflects the landed scrollLeft, not the pre-scroll state.
await waitForStable(() => getVisibleDayStart(page), {
stableFor: STABLE_FOR_MS,
timeout: STABLE_TIMEOUT_MS
})
}

async function scrollBy(page: Page, deltaX: number): Promise<void> {
Expand All @@ -22,8 +26,11 @@ async function scrollBy(page: Page, deltaX: number): Promise<void> {
.evaluate((el, dx) => (el as HTMLElement).scrollBy({ left: dx, behavior: 'auto' }), deltaX)
}

async function waitForScrollSettle(page: Page, ms = 400): Promise<void> {
await page.waitForTimeout(ms)
async function settledVisibleDayStart(page: Page): Promise<number> {
return waitForStable(() => getVisibleDayStart(page), {
stableFor: STABLE_FOR_MS,
timeout: STABLE_TIMEOUT_MS
})
}

test.describe('Calendar week view infinite horizontal scroll', () => {
Expand All @@ -32,88 +39,65 @@ test.describe('Calendar week view infinite horizontal scroll', () => {
await waitForVaultReady(page)
})

test('scrolls right and stays at the new position (no snap-back to today)', async ({ page }) => {
// #given — calendar opened on Week view
test('scrolling right advances the visible week and does not snap back', async ({ page }) => {
await openCalendarWeekView(page)
const initial = await getScrollLeft(page)
const initial = await getVisibleDayStart(page)

// #when — scroll right by ~2 day columns worth of pixels
await scrollBy(page, 400)
await waitForScrollSettle(page)
const afterScroll = await settledVisibleDayStart(page)
expect(afterScroll).toBeGreaterThan(initial)

// #then — scrollLeft advanced and did NOT snap back
const afterScroll = await getScrollLeft(page)
expect(afterScroll).toBeGreaterThan(initial + 100)

// #and — wait a bit longer; still no snap-back
await waitForScrollSettle(page, 600)
const afterWait = await getScrollLeft(page)
expect(afterWait).toBeGreaterThanOrEqual(afterScroll - 10)
// no snap-back: after a second settle window, value has not regressed below
// where it was before.
const stillAfter = await settledVisibleDayStart(page)
expect(stillAfter).toBeGreaterThanOrEqual(afterScroll)
})

test('scrolls left and stays at the new position', async ({ page }) => {
// #given — calendar opened on Week view, then user scrolled forward first
test('scrolling left brings the visible week back toward the origin', async ({ page }) => {
await openCalendarWeekView(page)
const initial = await getScrollLeft(page)
const initial = await getVisibleDayStart(page)

await scrollBy(page, 800)
await waitForScrollSettle(page)
const forward = await getScrollLeft(page)
expect(forward).toBeGreaterThan(initial + 400)
const afterForward = await settledVisibleDayStart(page)
expect(afterForward).toBeGreaterThan(initial)

// #when — scroll back left
await scrollBy(page, -500)
await waitForScrollSettle(page)

// #then — scrollLeft decreased and did not snap forward
const afterLeft = await getScrollLeft(page)
expect(afterLeft).toBeLessThan(forward - 200)
expect(afterLeft).toBeGreaterThan(initial - 10)

await waitForScrollSettle(page, 600)
const afterWait = await getScrollLeft(page)
expect(Math.abs(afterWait - afterLeft)).toBeLessThan(20)
const afterLeft = await settledVisibleDayStart(page)
expect(afterLeft).toBeLessThan(afterForward)
})

test('Today button smooth-scrolls back to todays week after scrolling forward', async ({
page
}) => {
// #given — user has scrolled far forward
test('Today button returns the visible week to the starting position', async ({ page }) => {
await openCalendarWeekView(page)
const initial = await getScrollLeft(page)
const initial = await getVisibleDayStart(page)

await scrollBy(page, 1200)
await waitForScrollSettle(page)
const advanced = await getScrollLeft(page)
expect(advanced).toBeGreaterThan(initial + 500)
const scrolledAway = await settledVisibleDayStart(page)
expect(scrolledAway).toBeGreaterThan(initial)

// #when — click Today
await page
.getByTestId('calendar-page')
.getByRole('button', { name: 'Today', exact: true })
.click()
await waitForScrollSettle(page, 800)

// #then — scroll returns near the original position
const afterToday = await getScrollLeft(page)
expect(Math.abs(afterToday - initial)).toBeLessThan(50)
const afterToday = await settledVisibleDayStart(page)
// Scroll↔anchor feedback can leave a 1-day rounding slack; anything tighter
// than that is product-internal and not user-observable.
expect(Math.abs(afterToday - initial)).toBeLessThanOrEqual(1)
})

test('Next button advances the visible week by 7 days worth of scroll', async ({ page }) => {
// #given — week view at today
test('Next button moves the visible week forward and Previous rewinds it', async ({ page }) => {
await openCalendarWeekView(page)
const initial = await getScrollLeft(page)
const initial = await getVisibleDayStart(page)

// #when — click Next
await page.getByTestId('calendar-page').getByRole('button', { name: 'Next period' }).click()
await waitForScrollSettle(page, 800)

// #then — scroll advanced by ~7 columns (lower bound to accommodate any column-width variance)
const afterNext = await getScrollLeft(page)
expect(afterNext).toBeGreaterThan(initial + 200)
const afterNext = await settledVisibleDayStart(page)
const nextDelta = afterNext - initial
expect(nextDelta).toBeGreaterThanOrEqual(1)
expect(nextDelta).toBeLessThanOrEqual(14)

// #and — Previous brings it back
await page.getByTestId('calendar-page').getByRole('button', { name: 'Previous period' }).click()
await waitForScrollSettle(page, 800)
const afterPrev = await getScrollLeft(page)
expect(Math.abs(afterPrev - initial)).toBeLessThan(50)
const afterPrev = await settledVisibleDayStart(page)
expect(afterPrev).toBeLessThan(afterNext)
expect(Math.abs(afterPrev - initial)).toBeLessThanOrEqual(1)
})
})
Loading
Loading