feature: Enhance output options with Google Sheets integration and improve Excel writer functionality

This commit is contained in:
Keith Solomon
2026-05-17 12:05:42 -05:00
parent 379526114c
commit a7cdcf95ae
10 changed files with 375 additions and 14 deletions
+4
View File
@@ -35,5 +35,9 @@ describe('ExcelWriter', () => {
const workbook = XLSX.readFile(path);
expect(workbook.SheetNames[0]).toBe('A Very Long Newsletter Name Tha');
expect(workbook.SheetNames[0].length).toBe(31);
const rows = XLSX.utils.sheet_to_json<Record<string, unknown>>(
workbook.Sheets[workbook.SheetNames[0]]
);
expect(rows[0]).not.toHaveProperty('Source Newsletter');
});
});
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest';
import { GoogleSheetsWriter } from '../src/output/googleSheets.js';
describe('GoogleSheetsWriter', () => {
it('creates missing sheets and appends content, sponsor, and dead-link rows', async () => {
const calls: unknown[] = [];
const sheets = {
spreadsheets: {
get: async () => ({
data: { sheets: [{ properties: { title: 'Sponsored Links' } }] }
}),
batchUpdate: async (request: unknown) => {
calls.push(request);
},
values: {
append: async (request: unknown) => {
calls.push(request);
}
}
}
};
await new GoogleSheetsWriter('sheet-1', undefined, sheets).write({
rows: [
{
'Source Newsletter': 'A Very Long Newsletter Name That Is Fine In Google Sheets',
Title: '=Formula',
'Link URL': 'https://example.com'
}
],
sponsors: [{ Newsletter: 'Weekly', Sponsor: 'Acme', Link: 'https://sponsor.example' }],
deadLinks: [{ URL: 'https://dead.example', Status: '404' }]
});
expect(calls[0]).toMatchObject({
spreadsheetId: 'sheet-1',
requestBody: {
requests: [
{
addSheet: {
properties: { title: 'A Very Long Newsletter Name That Is Fine In Google Sheets' }
}
},
{ addSheet: { properties: { title: 'Dead Links' } } }
]
}
});
expect(calls).toContainEqual(
expect.objectContaining({
spreadsheetId: 'sheet-1',
range: "'A Very Long Newsletter Name That Is Fine In Google Sheets'!A1",
requestBody: {
values: [
[
'Issue Date',
'Category',
'Link URL',
'Title',
'Description',
'Page Title + Meta',
'Also In'
],
['', '', 'https://example.com', "'=Formula", '', '', '']
]
}
})
);
expect(calls).toContainEqual(
expect.objectContaining({
range: "'Sponsored Links'!A1"
})
);
expect(calls).toContainEqual(
expect.objectContaining({
range: "'Dead Links'!A1"
})
);
});
});
+25
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { genericParser } from '../src/parsing/generic.js';
import { selectParser } from '../src/parsing/plugins.js';
describe('parser plugin selection', () => {
@@ -11,3 +12,27 @@ describe('parser plugin selection', () => {
).toBe('generic');
});
});
describe('generic parser', () => {
it('keeps descriptions local to each link when many links share a container', () => {
const links = genericParser.parse({
html: `
<div>
<h2>CSS & HTML Tools</h2>
<a href="https://cascade.example">Cascade</a> - CSS property icons.
<a href="https://frames.example">Fancy Frames</a> - Decorative border generator.
SPONSORED
<a href="https://flexboxle.example">flexboxle</a> - A daily puzzle game to master CSS Flexbox.
<a href="https://types.example">Typescale AI</a> - A typescale generator.
</div>
`
});
expect(links.map((link) => link.description)).toEqual([
'Cascade - CSS property icons.',
'Fancy Frames - Decorative border generator.',
'SPONSORED flexboxle - A daily puzzle game to master CSS Flexbox.',
'Typescale AI - A typescale generator.'
]);
});
});
+45
View File
@@ -41,4 +41,49 @@ describe('run orchestration', () => {
expect(result.linksExtracted).toBe(1);
expect(writes).toHaveLength(0);
});
it('only sends locally marked sponsored links to the sponsored output', async () => {
const stateFile = join(dir, 'state.json');
const writes: any[] = [];
await runCatalog({
config: {
gmail: { folder: 'Newsletters' },
output: { name: 'Catalog', excel: { enabled: true, path: join(dir, 'out.xlsx') } },
stateFile
},
messages: [
{
id: 'msg-1',
messageId: '<msg-1>',
from: 'Web Tools Weekly <w@example.com>',
date: '2026-05-16T00:00:00.000Z',
html: `
<div>
<a href="https://cascade.example">Cascade</a> - CSS property icons.
<a href="https://frames.example">Fancy Frames</a> - Decorative borders.
SPONSORED
<a href="https://flexboxle.example">flexboxle</a> - A daily puzzle game.
<a href="https://types.example">Typescale AI</a> - A typescale generator.
</div>
`
}
],
writers: [{ write: async (payload) => writes.push(payload) }]
});
expect(writes[0].sponsors).toEqual([
{
Newsletter: 'Web Tools Weekly',
Sponsor: 'flexboxle',
Link: 'https://flexboxle.example/',
Description: 'A daily puzzle game.'
}
]);
expect(writes[0].rows.map((row: any) => row.Title)).toEqual([
'Cascade',
'Fancy Frames',
'Typescale AI'
]);
});
});