From da90d1d50259dccfd1734da871d7547e19a8685c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:39:07 +0000 Subject: [PATCH 1/3] feat: prevent empty messages and optimistic posting Co-authored-by: benfoxall <51385+benfoxall@users.noreply.github.com> --- src/components/form.test.ts | 60 ++++++++++++++++++++++++++----------- src/components/form.ts | 40 ++++++++++++------------- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/components/form.test.ts b/src/components/form.test.ts index 64b194d..d3f34f3 100644 --- a/src/components/form.test.ts +++ b/src/components/form.test.ts @@ -8,23 +8,15 @@ customElements.define('cycloops-form', CycloopsForm, { extends: 'form' }) vi.mock('../db', () => ({ db: { notes: { - add: vi.fn(), + add: vi.fn().mockResolvedValue(1), + update: vi.fn().mockResolvedValue(1), }, }, })) // Mock geolocation const mockGeolocation = { - getCurrentPosition: vi.fn().mockImplementation((success) => - Promise.resolve( - success({ - coords: { - latitude: 50, - longitude: 50, - }, - }) - ) - ), + getCurrentPosition: vi.fn(), } vi.stubGlobal('navigator', { geolocation: mockGeolocation }) @@ -37,22 +29,54 @@ describe('CycloopsForm component', () => { form.innerHTML = '' document.body.appendChild(form) // This should trigger connectedCallback vi.clearAllMocks() + ;(db.notes.add as ReturnType).mockResolvedValue(1) + ;(db.notes.update as ReturnType).mockResolvedValue(1) }) - it('should add a note on submit', async () => { + it('should optimistically add a note immediately with placeholder coords', async () => { + mockGeolocation.getCurrentPosition.mockImplementation(() => { + // never resolves during this test + }) + const textarea = form.querySelector('textarea') as HTMLTextAreaElement textarea.value = 'Test message' - // The submit handler is async, so we need to wait for it to complete await form.submitHandler(new Event('submit')) expect(db.notes.add).toHaveBeenCalledOnce() expect(db.notes.add).toHaveBeenCalledWith( - expect.objectContaining({ - text: 'Test message', - lat: 50, - lon: 50, - }) + expect.objectContaining({ text: 'Test message', lat: 0, lon: 0 }) + ) + }) + + it('should update coords after geolocation resolves', async () => { + mockGeolocation.getCurrentPosition.mockImplementation((success: PositionCallback) => + success({ coords: { latitude: 50, longitude: 50 } } as GeolocationPosition) ) + + const textarea = form.querySelector('textarea') as HTMLTextAreaElement + textarea.value = 'Test message' + + await form.submitHandler(new Event('submit')) + + expect(db.notes.update).toHaveBeenCalledWith(1, { lat: 50, lon: 50 }) + }) + + it('should not add a note when message is empty', async () => { + const textarea = form.querySelector('textarea') as HTMLTextAreaElement + textarea.value = ' ' + + await form.submitHandler(new Event('submit')) + + expect(db.notes.add).not.toHaveBeenCalled() + }) + + it('should not add a note when message is blank', async () => { + const textarea = form.querySelector('textarea') as HTMLTextAreaElement + textarea.value = '' + + await form.submitHandler(new Event('submit')) + + expect(db.notes.add).not.toHaveBeenCalled() }) }) \ No newline at end of file diff --git a/src/components/form.ts b/src/components/form.ts index 119136d..238e510 100644 --- a/src/components/form.ts +++ b/src/components/form.ts @@ -19,30 +19,28 @@ export class CycloopsForm extends HTMLFormElement { e.preventDefault(); const data = new FormData(this); - const text = data.get("message") as string; - const time = Date.now(); + const text = (data.get("message") as string).trim(); - const [lat, lon] = await new Promise<[number, number]>( - (resolve, reject) => { - navigator.geolocation.getCurrentPosition( - (position) => { - console.log(position.coords); - resolve([position.coords.latitude, position.coords.longitude]); - }, - () => { - resolve([0, 0]); - } - ); - } - ); + if (!text) return; + + const time = Date.now(); - await db.notes.add({ - time, - text, - lat, - lon, - }); + // Optimistically add the note immediately so it appears in the list + const id = await db.notes.add({ time, text, lat: 0, lon: 0 }); this.reset(); + + // Update with real coordinates once geolocation resolves + navigator.geolocation.getCurrentPosition( + async (position) => { + await db.notes.update(id, { + lat: position.coords.latitude, + lon: position.coords.longitude, + }); + }, + async () => { + // Keep lat/lon as 0,0 on failure — note is already saved + } + ); } } From 482035eaebfa43e2756116981f41fb837a80049c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:40:23 +0000 Subject: [PATCH 2/3] fix: review feedback - remove unnecessary async, clarify test name Co-authored-by: benfoxall <51385+benfoxall@users.noreply.github.com> --- src/components/form.test.ts | 2 +- src/components/form.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/form.test.ts b/src/components/form.test.ts index d3f34f3..435ed4d 100644 --- a/src/components/form.test.ts +++ b/src/components/form.test.ts @@ -62,7 +62,7 @@ describe('CycloopsForm component', () => { expect(db.notes.update).toHaveBeenCalledWith(1, { lat: 50, lon: 50 }) }) - it('should not add a note when message is empty', async () => { + it('should not add a note when message is whitespace only', async () => { const textarea = form.querySelector('textarea') as HTMLTextAreaElement textarea.value = ' ' diff --git a/src/components/form.ts b/src/components/form.ts index 238e510..a8843e6 100644 --- a/src/components/form.ts +++ b/src/components/form.ts @@ -38,7 +38,7 @@ export class CycloopsForm extends HTMLFormElement { lon: position.coords.longitude, }); }, - async () => { + () => { // Keep lat/lon as 0,0 on failure — note is already saved } ); From 0c8122ecd35a03a215378264a5d16134160794e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:18:51 +0000 Subject: [PATCH 3/3] fix: exclude placeholder 0,0 coords from map noteLocations Co-authored-by: benfoxall <51385+benfoxall@users.noreply.github.com> --- src/components/map.test.ts | 7 +++++++ src/components/map.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/map.test.ts b/src/components/map.test.ts index 9520810..f8f84f7 100644 --- a/src/components/map.test.ts +++ b/src/components/map.test.ts @@ -21,6 +21,13 @@ describe('map computed signals', () => { expect(noteLocations.value.features[1].properties.id).toBe(2) }) + it('noteLocations should exclude notes with placeholder 0,0 coordinates', () => { + const placeholder: Note = { id: 3, time: Date.now(), text: 'Pending', lat: 0, lon: 0 } + notes.value = [note1, placeholder] + expect(noteLocations.value.features.length).toBe(1) + expect(noteLocations.value.features[0].properties.id).toBe(1) + }) + it('visibleLocations should filter notes based on visibility', () => { notes.value = [note1, note2] visible.value = new Set([1]) diff --git a/src/components/map.ts b/src/components/map.ts index ff05366..394ec7d 100644 --- a/src/components/map.ts +++ b/src/components/map.ts @@ -11,7 +11,9 @@ type Locations = GeoJSON.FeatureCollection< export const noteLocations = computed(() => ({ type: "FeatureCollection", - features: notes.value.map((note) => ({ + features: notes.value + .filter((note) => note.lat !== 0 || note.lon !== 0) + .map((note) => ({ type: "Feature", properties: { id: note.id,