@@ -10,6 +10,13 @@ import {
1010 parseActivities ,
1111 parseOrganizers ,
1212 generateUniqueUid ,
13+ escapeTableCell ,
14+ eventDataToDisplayMap ,
15+ buildNewEventTable ,
16+ buildEditEventTable ,
17+ formatLongDescription ,
18+ buildPlusCodeNoteBlocks ,
19+ buildPrBody ,
1320} from './event-issue-helpers.mjs' ;
1421
1522// ── parseIssueSections ────────────────────────────────────────────────────────
@@ -178,3 +185,235 @@ describe('generateUniqueUid', () => {
178185 assert . equal ( uids . size , 20 ) ;
179186 } ) ;
180187} ) ;
188+
189+ // ── escapeTableCell ───────────────────────────────────────────────────────────
190+
191+ describe ( 'escapeTableCell' , ( ) => {
192+ test ( 'escapes pipe characters' , ( ) => {
193+ assert . equal ( escapeTableCell ( 'a | b' ) , 'a \\| b' ) ;
194+ } ) ;
195+
196+ test ( 'replaces newlines with spaces' , ( ) => {
197+ assert . equal ( escapeTableCell ( 'line1\nline2' ) , 'line1 line2' ) ;
198+ } ) ;
199+
200+ test ( 'handles null/undefined as empty string' , ( ) => {
201+ assert . equal ( escapeTableCell ( null ) , '' ) ;
202+ assert . equal ( escapeTableCell ( undefined ) , '' ) ;
203+ } ) ;
204+ } ) ;
205+
206+ // ── eventDataToDisplayMap ─────────────────────────────────────────────────────
207+
208+ describe ( 'eventDataToDisplayMap' , ( ) => {
209+ const baseRecord = {
210+ event_name : 'PCD @ Tokyo' ,
211+ primary_contact : { name : 'Jane Doe' , email : 'jane@example.com' } ,
212+ online_event : false ,
213+ event_date : '' ,
214+ event_end_date : undefined ,
215+ event_start_time : '' ,
216+ event_end_time : '' ,
217+ event_location : { address : '123 Main St' , plus_code : '8FW4V75V+8Q' } ,
218+ city : 'Tokyo' ,
219+ country : 'Japan' ,
220+ organization_name : '' ,
221+ organization_url : '' ,
222+ organization_type : '' ,
223+ event_url : '' ,
224+ event_page_url : '' ,
225+ forum_thread_url : '' ,
226+ event_short_description : 'A gathering.' ,
227+ event_activities : [ 'Live coding' , 'Exhibition' ] ,
228+ organizers : [ { name : 'Jane Doe' } , { name : 'John Smith' } ] ,
229+ } ;
230+
231+ test ( 'returns Map with correct labels and values' , ( ) => {
232+ const map = eventDataToDisplayMap ( baseRecord ) ;
233+ assert . equal ( map . get ( 'Event name' ) , 'PCD @ Tokyo' ) ;
234+ assert . equal ( map . get ( 'Contact' ) , 'Jane Doe (jane@example.com)' ) ;
235+ assert . equal ( map . get ( 'Format' ) , 'In person' ) ;
236+ assert . equal ( map . get ( 'City' ) , 'Tokyo' ) ;
237+ assert . equal ( map . get ( 'Country' ) , 'Japan' ) ;
238+ assert . equal ( map . get ( 'Address' ) , '123 Main St' ) ;
239+ assert . equal ( map . get ( 'Plus Code' ) , '`8FW4V75V+8Q`' ) ;
240+ assert . equal ( map . get ( 'Short description' ) , 'A gathering.' ) ;
241+ assert . equal ( map . get ( 'Organizers' ) , 'Jane Doe, John Smith' ) ;
242+ } ) ;
243+
244+ test ( 'empty string maps to _No response_' , ( ) => {
245+ const map = eventDataToDisplayMap ( baseRecord ) ;
246+ assert . equal ( map . get ( 'Date' ) , '_No response_' ) ;
247+ assert . equal ( map . get ( 'Organization' ) , '_No response_' ) ;
248+ } ) ;
249+
250+ test ( 'undefined maps to _No response_' , ( ) => {
251+ const map = eventDataToDisplayMap ( baseRecord ) ;
252+ assert . equal ( map . get ( 'End date' ) , '_No response_' ) ;
253+ } ) ;
254+
255+ test ( 'false is NOT treated as blank (Format: Online)' , ( ) => {
256+ const map = eventDataToDisplayMap ( { ...baseRecord , online_event : false } ) ;
257+ assert . equal ( map . get ( 'Format' ) , 'In person' ) ;
258+ } ) ;
259+
260+ test ( 'activities are sorted for deterministic output' , ( ) => {
261+ const map = eventDataToDisplayMap ( { ...baseRecord , event_activities : [ 'Exhibition' , 'Live coding' ] } ) ;
262+ assert . equal ( map . get ( 'Activities' ) , 'Exhibition, Live coding' ) ;
263+ } ) ;
264+ } ) ;
265+
266+ // ── buildNewEventTable ────────────────────────────────────────────────────────
267+
268+ describe ( 'buildNewEventTable' , ( ) => {
269+ test ( 'produces correct markdown table with Field/Value headers' , ( ) => {
270+ const record = {
271+ event_name : 'PCD @ Tokyo' ,
272+ primary_contact : { name : 'Jane' , email : 'jane@example.com' } ,
273+ online_event : false ,
274+ event_date : '' , event_end_date : undefined , event_start_time : '' , event_end_time : '' ,
275+ event_location : { address : '' , plus_code : '' } ,
276+ city : '' , country : '' , organization_name : '' , organization_url : '' ,
277+ organization_type : '' , event_url : '' , event_page_url : '' , forum_thread_url : '' ,
278+ event_short_description : '' , event_activities : [ ] , organizers : [ ] ,
279+ } ;
280+ const table = buildNewEventTable ( eventDataToDisplayMap ( record ) ) ;
281+ assert . ok ( table . startsWith ( '| Field | Value |' ) , 'should start with header row' ) ;
282+ assert . ok ( table . includes ( '|---|---|' ) , 'should include separator' ) ;
283+ assert . ok ( table . includes ( '| Event name | PCD @ Tokyo |' ) , 'should include event name row' ) ;
284+ } ) ;
285+ } ) ;
286+
287+ // ── buildEditEventTable ───────────────────────────────────────────────────────
288+
289+ describe ( 'buildEditEventTable' , ( ) => {
290+ const baseRecord = {
291+ event_name : 'PCD @ Tokyo' ,
292+ primary_contact : { name : 'Jane' , email : 'jane@example.com' } ,
293+ online_event : false ,
294+ event_date : '2026-03-21' , event_end_date : undefined , event_start_time : '' , event_end_time : '' ,
295+ event_location : { address : '123 Main St' , plus_code : '8FW4V75V+8Q' } ,
296+ city : 'Tokyo' , country : 'Japan' , organization_name : '' , organization_url : '' ,
297+ organization_type : '' , event_url : '' , event_page_url : '' , forum_thread_url : '' ,
298+ event_short_description : 'A gathering.' , event_activities : [ 'Live coding' ] , organizers : [ ] ,
299+ } ;
300+
301+ test ( 'only includes rows where values differ' , ( ) => {
302+ const prev = eventDataToDisplayMap ( { ...baseRecord , event_date : '2026-01-01' } ) ;
303+ const next = eventDataToDisplayMap ( { ...baseRecord , event_date : '2026-03-21' } ) ;
304+ const table = buildEditEventTable ( prev , next ) ;
305+ assert . ok ( table . includes ( '| Field | Previous | New |' ) , 'should have 3-column header' ) ;
306+ assert . ok ( table . includes ( '| Date |' ) , 'should include changed Date row' ) ;
307+ assert . ok ( ! table . includes ( '| Event name |' ) , 'should not include unchanged Event name' ) ;
308+ } ) ;
309+
310+ test ( 'returns fallback message when nothing changed' , ( ) => {
311+ const map = eventDataToDisplayMap ( baseRecord ) ;
312+ assert . equal ( buildEditEventTable ( map , map ) , '_No metadata changes._' ) ;
313+ } ) ;
314+
315+ test ( 'activities compared deterministically (order-insensitive)' , ( ) => {
316+ const prev = eventDataToDisplayMap ( { ...baseRecord , event_activities : [ 'Exhibition' , 'Live coding' ] } ) ;
317+ const next = eventDataToDisplayMap ( { ...baseRecord , event_activities : [ 'Live coding' , 'Exhibition' ] } ) ;
318+ assert . equal ( buildEditEventTable ( prev , next ) , '_No metadata changes._' ) ;
319+ } ) ;
320+ } ) ;
321+
322+ // ── formatLongDescription ─────────────────────────────────────────────────────
323+
324+ describe ( 'formatLongDescription' , ( ) => {
325+ test ( 'prefixes each line with > ' , ( ) => {
326+ assert . equal ( formatLongDescription ( 'line1\nline2' ) , '> line1\n> line2' ) ;
327+ } ) ;
328+
329+ test ( 'single line' , ( ) => {
330+ assert . equal ( formatLongDescription ( 'Hello world' ) , '> Hello world' ) ;
331+ } ) ;
332+
333+ test ( 'blank/falsy returns _No response_' , ( ) => {
334+ assert . equal ( formatLongDescription ( '' ) , '_No response_' ) ;
335+ assert . equal ( formatLongDescription ( null ) , '_No response_' ) ;
336+ assert . equal ( formatLongDescription ( undefined ) , '_No response_' ) ;
337+ assert . equal ( formatLongDescription ( ' ' ) , '_No response_' ) ;
338+ } ) ;
339+
340+ test ( 'normalizes \\r\\n to \\n' , ( ) => {
341+ assert . equal ( formatLongDescription ( 'line1\r\nline2' ) , '> line1\n> line2' ) ;
342+ } ) ;
343+ } ) ;
344+
345+ // ── buildPlusCodeNoteBlocks ──────────────────────────────────────────────────
346+
347+ describe ( 'buildPlusCodeNoteBlocks' , ( ) => {
348+ test ( 'returns note block when plusCodeNote is truthy' , ( ) => {
349+ const blocks = buildPlusCodeNoteBlocks ( true , 'V75V+8Q' , '8FW4V75V+8Q' ) ;
350+ assert . equal ( blocks . length , 1 ) ;
351+ assert . ok ( blocks [ 0 ] . includes ( '> [!NOTE]' ) ) ;
352+ assert . ok ( blocks [ 0 ] . includes ( '`V75V+8Q`' ) ) ;
353+ assert . ok ( blocks [ 0 ] . includes ( 'https://plus.codes/8FW4V75V+8Q' ) ) ;
354+ } ) ;
355+
356+ test ( 'returns empty array when plusCodeNote is falsy' , ( ) => {
357+ assert . deepEqual ( buildPlusCodeNoteBlocks ( false , 'V75V+8Q' , '8FW4V75V+8Q' ) , [ ] ) ;
358+ assert . deepEqual ( buildPlusCodeNoteBlocks ( null , 'V75V+8Q' , '8FW4V75V+8Q' ) , [ ] ) ;
359+ } ) ;
360+ } ) ;
361+
362+ // ── buildPrBody ───────────────────────────────────────────────────────────────
363+
364+ describe ( 'buildPrBody' , ( ) => {
365+ const base = {
366+ number : 42 ,
367+ eventName : 'PCD @ Tokyo' ,
368+ submitterLogin : 'someuser' ,
369+ plusCodeForLink : '8FW4V75V+8Q' ,
370+ dataTable : '| Field | Value |\n|---|---|\n| Event name | PCD @ Tokyo |' ,
371+ longDescriptionSection : null ,
372+ noteBlocks : [ ] ,
373+ } ;
374+
375+ test ( 'new event body has correct structure' , ( ) => {
376+ const body = buildPrBody ( { mode : 'new' , ...base } ) ;
377+ assert . ok ( body . includes ( 'Closes #42' ) ) ;
378+ assert . ok ( body . includes ( '"New Event" issue form' ) ) ;
379+ assert . ok ( body . includes ( '### Review checklist' ) ) ;
380+ assert . ok ( body . includes ( 'The event data below is accurate' ) ) ;
381+ assert . ok ( body . includes ( 'https://plus.codes/8FW4V75V+8Q' ) ) ;
382+ assert . ok ( body . includes ( '### Event data' ) ) ;
383+ assert . ok ( body . includes ( '| Field | Value |' ) ) ;
384+ assert . ok ( ! body . includes ( '### Long description' ) , 'should not include long description section when null' ) ;
385+ } ) ;
386+
387+ test ( 'edit event body has correct structure' , ( ) => {
388+ const body = buildPrBody ( { mode : 'edit' , ...base , dataTable : '| Field | Previous | New |\n|---|---|---|\n| Date | old | new |' } ) ;
389+ assert . ok ( body . includes ( '"Edit Event" issue form' ) ) ;
390+ assert . ok ( body . includes ( 'The changes below are correct' ) ) ;
391+ assert . ok ( body . includes ( '### Changes' ) ) ;
392+ } ) ;
393+
394+ test ( 'long description section included when provided' , ( ) => {
395+ const body = buildPrBody ( { mode : 'new' , ...base , longDescriptionSection : '> Some description.' } ) ;
396+ assert . ok ( body . includes ( '### Long description' ) ) ;
397+ assert . ok ( body . includes ( '> Some description.' ) ) ;
398+ } ) ;
399+
400+ test ( 'noteBlocks appear before Closes line' , ( ) => {
401+ const body = buildPrBody ( {
402+ mode : 'new' , ...base ,
403+ noteBlocks : [ '> [!NOTE]\n> The Plus Code was auto-recovered.' ] ,
404+ } ) ;
405+ const noteIdx = body . indexOf ( '> [!NOTE]' ) ;
406+ const closesIdx = body . indexOf ( 'Closes #42' ) ;
407+ assert . ok ( noteIdx < closesIdx , 'note should appear before Closes line' ) ;
408+ } ) ;
409+
410+ test ( 'submitter shown as @login when provided' , ( ) => {
411+ const body = buildPrBody ( { mode : 'new' , ...base } ) ;
412+ assert . ok ( body . includes ( 'Submitted by @someuser' ) ) ;
413+ } ) ;
414+
415+ test ( 'submitter shown as "the submitter" when login is empty' , ( ) => {
416+ const body = buildPrBody ( { mode : 'new' , ...base , submitterLogin : '' } ) ;
417+ assert . ok ( body . includes ( 'Submitted by the submitter' ) ) ;
418+ } ) ;
419+ } ) ;
0 commit comments