From f66ac8a58956e98376db1044cdd48454318bf217 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 08:13:53 -0800 Subject: [PATCH 01/25] ci(code-coverage): add code coverage reporting to edge app workflow - Update test:coverage script to generate lcov format output - Add workflow steps to run coverage tests and generate HTML reports - Upload coverage reports as downloadable artifacts per app Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 23 +++++++++++++++++++++++ edge-apps/edge-apps-library/package.json | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index 024fb997f..82d57806a 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -161,6 +161,29 @@ jobs: working-directory: edge-apps/${{ matrix.app }} run: bun run test:unit + - name: Check for coverage script + id: check-coverage + working-directory: edge-apps/${{ matrix.app }} + run: | + if grep -q '"test:coverage"' package.json; then + echo "has-coverage=true" >> "$GITHUB_OUTPUT" + else + echo "has-coverage=false" >> "$GITHUB_OUTPUT" + fi + + - name: Run coverage tests + if: steps.check-coverage.outputs.has-coverage == 'true' + working-directory: edge-apps/${{ matrix.app }} + run: bun run test:coverage + + - name: Report coverage to summary + if: steps.check-coverage.outputs.has-coverage == 'true' + uses: romeovs/lcov-reporter-action@v0.3.1 + with: + lcov-file: edge-apps/${{ matrix.app }}/coverage/lcov.info + github-token: ${{ secrets.GITHUB_TOKEN }} + title: Coverage Report - ${{ matrix.app }} + - name: Build application working-directory: edge-apps/${{ matrix.app }} run: bun run build diff --git a/edge-apps/edge-apps-library/package.json b/edge-apps/edge-apps-library/package.json index e009928b3..e5afaf9c5 100644 --- a/edge-apps/edge-apps-library/package.json +++ b/edge-apps/edge-apps-library/package.json @@ -39,7 +39,7 @@ "build:types": "tsc", "test": "bun test --parallel", "test:unit": "bun test --parallel", - "test:coverage": "bun test --coverage --parallel", + "test:coverage": "bun test --parallel --coverage --coverage-reporter=lcov", "lint": "eslint . --fix", "format": "prettier --write src/ scripts/ README.md index.html", "format:check": "prettier --check src/ scripts/ README.md index.html", From d37d2f9b12ce094421aeb84ccb1c258b90135a21 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 09:22:54 -0800 Subject: [PATCH 02/25] fix(ci): simplify coverage workflow to run tests only Remove coverage report generation steps. The workflow now only runs coverage tests without generating or displaying reports. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 8 -------- edge-apps/edge-apps-library/package.json | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index 82d57806a..9c342d7aa 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -176,14 +176,6 @@ jobs: working-directory: edge-apps/${{ matrix.app }} run: bun run test:coverage - - name: Report coverage to summary - if: steps.check-coverage.outputs.has-coverage == 'true' - uses: romeovs/lcov-reporter-action@v0.3.1 - with: - lcov-file: edge-apps/${{ matrix.app }}/coverage/lcov.info - github-token: ${{ secrets.GITHUB_TOKEN }} - title: Coverage Report - ${{ matrix.app }} - - name: Build application working-directory: edge-apps/${{ matrix.app }} run: bun run build diff --git a/edge-apps/edge-apps-library/package.json b/edge-apps/edge-apps-library/package.json index e5afaf9c5..ea268fc3e 100644 --- a/edge-apps/edge-apps-library/package.json +++ b/edge-apps/edge-apps-library/package.json @@ -39,7 +39,7 @@ "build:types": "tsc", "test": "bun test --parallel", "test:unit": "bun test --parallel", - "test:coverage": "bun test --parallel --coverage --coverage-reporter=lcov", + "test:coverage": "bun test --coverage --parallel --coverage-reporter=text", "lint": "eslint . --fix", "format": "prettier --write src/ scripts/ README.md index.html", "format:check": "prettier --check src/ scripts/ README.md index.html", From 925768ba4bc247bd04f774977a373923c6a392b9 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 10:17:57 -0800 Subject: [PATCH 03/25] feat(ci): enable automatic coverage checks with 90% threshold - Configure bunfig.toml to always run coverage with 90% threshold - Remove separate coverage check steps from workflow - Coverage is now enforced automatically during unit tests Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 15 --------------- edge-apps/edge-apps-library/bunfig.toml | 2 ++ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index 9c342d7aa..024fb997f 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -161,21 +161,6 @@ jobs: working-directory: edge-apps/${{ matrix.app }} run: bun run test:unit - - name: Check for coverage script - id: check-coverage - working-directory: edge-apps/${{ matrix.app }} - run: | - if grep -q '"test:coverage"' package.json; then - echo "has-coverage=true" >> "$GITHUB_OUTPUT" - else - echo "has-coverage=false" >> "$GITHUB_OUTPUT" - fi - - - name: Run coverage tests - if: steps.check-coverage.outputs.has-coverage == 'true' - working-directory: edge-apps/${{ matrix.app }} - run: bun run test:coverage - - name: Build application working-directory: edge-apps/${{ matrix.app }} run: bun run build diff --git a/edge-apps/edge-apps-library/bunfig.toml b/edge-apps/edge-apps-library/bunfig.toml index 38b6d6fe7..6c7b562de 100644 --- a/edge-apps/edge-apps-library/bunfig.toml +++ b/edge-apps/edge-apps-library/bunfig.toml @@ -1,3 +1,5 @@ [test] preload = ["./src/test/setup.ts"] +coverage = true +coverageThreshold = 0.9 From 060f439304dcfac61f5a7a6a397455b5fa67f2c6 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 11:12:20 -0800 Subject: [PATCH 04/25] test(edge-apps-library): improve test coverage - Add comprehensive theme and branding tests - Refactor tests to eliminate duplication - Fix jsdom compatibility for FileReader and Blob APIs Co-Authored-By: Claude Sonnet 4.5 --- .../edge-apps-library/src/test/mock.test.ts | 90 +++-- edge-apps/edge-apps-library/src/test/setup.ts | 1 + .../edge-apps-library/src/utils/theme.test.ts | 316 ++++++++++++++---- 3 files changed, 312 insertions(+), 95 deletions(-) diff --git a/edge-apps/edge-apps-library/src/test/mock.test.ts b/edge-apps/edge-apps-library/src/test/mock.test.ts index 53e44ce09..eb8cd83c6 100644 --- a/edge-apps/edge-apps-library/src/test/mock.test.ts +++ b/edge-apps/edge-apps-library/src/test/mock.test.ts @@ -9,6 +9,15 @@ import { const global = globalThis as Record +// Helper function to check property +function expectProperty(obj: unknown, key: string, value?: unknown) { + if (value !== undefined) { + expect(obj).toHaveProperty(key, value) + } else { + expect(obj).toHaveProperty(key) + } +} + // eslint-disable-next-line max-lines-per-function describe('mock utilities', () => { afterEach(() => { @@ -17,22 +26,36 @@ describe('mock utilities', () => { describe('mockMetadata', () => { test('should have default metadata values', () => { - expect(mockMetadata).toHaveProperty('coordinates') - expect(mockMetadata).toHaveProperty('hostname', 'test-hostname') - expect(mockMetadata).toHaveProperty('location', 'Test Location') - expect(mockMetadata).toHaveProperty('hardware', 'test-hardware') - expect(mockMetadata).toHaveProperty('screenly_version', '1.0.0-test') - expect(mockMetadata).toHaveProperty('screen_name', 'Test Screen') - expect(mockMetadata).toHaveProperty('tags') + const expectedProperties = [ + { key: 'coordinates' }, + { key: 'hostname', value: 'test-hostname' }, + { key: 'location', value: 'Test Location' }, + { key: 'hardware', value: 'test-hardware' }, + { key: 'screenly_version', value: '1.0.0-test' }, + { key: 'screen_name', value: 'Test Screen' }, + { key: 'tags' }, + ] + + expectedProperties.forEach(({ key, value }) => { + expectProperty(mockMetadata, key, value) + }) + expect(Array.isArray(mockMetadata.tags)).toBe(true) }) }) describe('mockSettings', () => { test('should have default settings values', () => { - expect(mockSettings).toHaveProperty('screenly_color_accent', '#972EFF') - expect(mockSettings).toHaveProperty('screenly_color_light', '#ADAFBE') - expect(mockSettings).toHaveProperty('screenly_color_dark', '#454BD2') + const expectedProperties = [ + { key: 'screenly_color_accent', value: '#972EFF' }, + { key: 'screenly_color_light', value: '#ADAFBE' }, + { key: 'screenly_color_dark', value: '#454BD2' }, + ] + + expectedProperties.forEach(({ key, value }) => { + expectProperty(mockSettings, key, value) + }) + expect(mockSettings).not.toHaveProperty('theme') }) }) @@ -41,36 +64,44 @@ describe('mock utilities', () => { test('should create mock with default values', () => { const mock = createMockScreenly() - expect(mock).toHaveProperty('signalReadyForRendering') - expect(mock).toHaveProperty('metadata') - expect(mock).toHaveProperty('settings') - expect(mock).toHaveProperty('cors_proxy_url', 'http://localhost:8080') + const expectedProperties = [ + 'signalReadyForRendering', + 'metadata', + 'settings', + ] + expectedProperties.forEach((key) => expectProperty(mock, key)) + + expectProperty(mock, 'cors_proxy_url', 'http://localhost:8080') expect(typeof mock.signalReadyForRendering).toBe('function') }) + test('should have a callable signalReadyForRendering function', () => { + const mock = createMockScreenly() + expect(() => mock.signalReadyForRendering()).not.toThrow() + }) + test('should merge custom metadata', () => { - const mock = createMockScreenly({ + const customMetadata = { hostname: 'custom-hostname', location: 'Custom Location', - }) + } + const mock = createMockScreenly(customMetadata) expect(mock.metadata.hostname).toBe('custom-hostname') expect(mock.metadata.location).toBe('Custom Location') - expect(mock.metadata.hardware).toBe('test-hardware') // default value preserved + expect(mock.metadata.hardware).toBe('test-hardware') }) test('should merge custom settings', () => { - const mock = createMockScreenly( - {}, - { - theme: 'dark', - custom_setting: 'custom_value', - }, - ) + const customSettings = { + theme: 'dark', + custom_setting: 'custom_value', + } + const mock = createMockScreenly({}, customSettings) expect(mock.settings.theme).toBe('dark') expect(mock.settings.custom_setting).toBe('custom_value') - expect(mock.settings.screenly_color_accent).toBe('#972EFF') // default value preserved + expect(mock.settings.screenly_color_accent).toBe('#972EFF') }) }) @@ -79,21 +110,20 @@ describe('mock utilities', () => { setupScreenlyMock() expect(global.screenly).toBeDefined() - expect(global.screenly).toHaveProperty('metadata') - expect(global.screenly).toHaveProperty('settings') + expectProperty(global.screenly, 'metadata') + expectProperty(global.screenly, 'settings') }) test('should setup with custom values', () => { setupScreenlyMock({ hostname: 'custom-host' }, { theme: 'dark' }) const screenly = global.screenly as Record - expect(screenly.metadata).toHaveProperty('hostname', 'custom-host') - expect(screenly.settings).toHaveProperty('theme', 'dark') + expectProperty(screenly.metadata, 'hostname', 'custom-host') + expectProperty(screenly.settings, 'theme', 'dark') }) test('should return the mock object', () => { const mock = setupScreenlyMock() - expect(mock).toBe(global.screenly) }) }) diff --git a/edge-apps/edge-apps-library/src/test/setup.ts b/edge-apps/edge-apps-library/src/test/setup.ts index 18c65f9d0..8d2cdcdbb 100644 --- a/edge-apps/edge-apps-library/src/test/setup.ts +++ b/edge-apps/edge-apps-library/src/test/setup.ts @@ -12,3 +12,4 @@ global.document = dom.window.document global.window = dom.window as unknown as Window & typeof globalThis global.navigator = dom.window.navigator global.Node = dom.window.Node +global.FileReader = dom.window.FileReader diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index 53b807f3e..c652ec52b 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -1,14 +1,50 @@ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test' +import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' import { getPrimaryColor, getSecondaryColor, getThemeColors, applyThemeColors, setupTheme, + fetchLogoImage, + setupBrandingLogo, + setupBranding, DEFAULT_THEME_COLORS, } from './theme' import { setupScreenlyMock, resetScreenlyMock } from '../test/mock' +// Helper constants +const PNG_MAGIC_BYTES = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, +]) +const JPEG_MAGIC_BYTES = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]) +const DEFAULT_SECONDARY = '#adafbe' +const PROXY_URL = 'https://proxy.example.com' +const TEST_LOGO_URL = 'https://example.com/logo.png' + +// Helper functions +function createMockFetch(ok: boolean, bytes?: Uint8Array, status = 200) { + return mock(() => + Promise.resolve({ + ok, + status, + blob: bytes ? () => Promise.resolve(new Blob([bytes])) : undefined, + } as Response), + ) +} + +function getCSSProperty(name: string): string { + return document.documentElement.style.getPropertyValue(name) +} + +function setupMockWithLogo(theme: 'light' | 'dark', logoUrl: string) { + const logoKey = + theme === 'light' ? 'screenly_logo_light' : 'screenly_logo_dark' + setupScreenlyMock( + { cors_proxy_url: PROXY_URL }, + { theme, [logoKey]: logoUrl }, + ) +} + // eslint-disable-next-line max-lines-per-function describe('theme utilities', () => { beforeEach(() => { @@ -20,51 +56,75 @@ describe('theme utilities', () => { }) describe('getPrimaryColor', () => { - test('should return default color when no accent color provided', () => { - const color = getPrimaryColor() - expect(color).toBe(DEFAULT_THEME_COLORS.primary) - }) - - test('should return default color when accent color is white', () => { - const color = getPrimaryColor('#ffffff') - expect(color).toBe(DEFAULT_THEME_COLORS.primary) - }) + const testCases = [ + { + input: undefined, + expected: DEFAULT_THEME_COLORS.primary, + desc: 'no accent color provided', + }, + { + input: '#ffffff', + expected: DEFAULT_THEME_COLORS.primary, + desc: 'accent color is white', + }, + { + input: '#FFFFFF', + expected: DEFAULT_THEME_COLORS.primary, + desc: 'accent color is white (uppercase)', + }, + { input: '#FF0000', expected: '#FF0000', desc: 'provided accent color' }, + ] - test('should return default color when accent color is white (uppercase)', () => { - const color = getPrimaryColor('#FFFFFF') - expect(color).toBe(DEFAULT_THEME_COLORS.primary) - }) - - test('should return provided accent color', () => { - const color = getPrimaryColor('#FF0000') - expect(color).toBe('#FF0000') + testCases.forEach(({ input, expected, desc }) => { + test(`should return ${desc === 'provided accent color' ? 'provided' : 'default'} color when ${desc}`, () => { + expect(getPrimaryColor(input)).toBe(expected) + }) }) }) describe('getSecondaryColor', () => { - test('should return default secondary when theme is undefined', () => { - const color = getSecondaryColor(undefined) - expect(color).toBe(DEFAULT_THEME_COLORS.secondary) - }) - - test('should return light color when theme is light', () => { - const color = getSecondaryColor('light', '#123456') - expect(color).toBe('#123456') - }) - - test('should return dark color when theme is dark', () => { - const color = getSecondaryColor('dark', undefined, '#654321') - expect(color).toBe('#654321') - }) - - test('should return default when light color is white', () => { - const color = getSecondaryColor('light', '#ffffff') - expect(color).toBe('#adafbe') - }) + const testCases = [ + { + theme: undefined, + light: undefined, + dark: undefined, + expected: DEFAULT_THEME_COLORS.secondary, + desc: 'theme is undefined', + }, + { + theme: 'light' as const, + light: '#123456', + dark: undefined, + expected: '#123456', + desc: 'theme is light', + }, + { + theme: 'dark' as const, + light: undefined, + dark: '#654321', + expected: '#654321', + desc: 'theme is dark', + }, + { + theme: 'light' as const, + light: '#ffffff', + dark: undefined, + expected: DEFAULT_SECONDARY, + desc: 'light color is white', + }, + { + theme: 'dark' as const, + light: undefined, + dark: '#FFFFFF', + expected: DEFAULT_SECONDARY, + desc: 'dark color is white', + }, + ] - test('should return default when dark color is white', () => { - const color = getSecondaryColor('dark', undefined, '#FFFFFF') - expect(color).toBe('#adafbe') + testCases.forEach(({ theme, light, dark, expected, desc }) => { + test(`should return ${desc.includes('white') ? 'default' : light || dark || 'default secondary'} when ${desc}`, () => { + expect(getSecondaryColor(theme, light, dark)).toBe(expected) + }) }) }) @@ -106,26 +166,10 @@ describe('theme utilities', () => { applyThemeColors(colors) - expect( - document.documentElement.style.getPropertyValue( - '--theme-color-primary', - ), - ).toBe('#FF0000') - expect( - document.documentElement.style.getPropertyValue( - '--theme-color-secondary', - ), - ).toBe('#00FF00') - expect( - document.documentElement.style.getPropertyValue( - '--theme-color-tertiary', - ), - ).toBe('#0000FF') - expect( - document.documentElement.style.getPropertyValue( - '--theme-color-background', - ), - ).toBe('#FFFFFF') + expect(getCSSProperty('--theme-color-primary')).toBe('#FF0000') + expect(getCSSProperty('--theme-color-secondary')).toBe('#00FF00') + expect(getCSSProperty('--theme-color-tertiary')).toBe('#0000FF') + expect(getCSSProperty('--theme-color-background')).toBe('#FFFFFF') }) }) @@ -144,11 +188,153 @@ describe('theme utilities', () => { expect(colors.primary).toBe('#FF0000') expect(colors.secondary).toBe('#00FF00') - expect( - document.documentElement.style.getPropertyValue( - '--theme-color-primary', - ), - ).toBe('#FF0000') + expect(getCSSProperty('--theme-color-primary')).toBe('#FF0000') + }) + }) + + describe('fetchLogoImage', () => { + test('should fetch and process SVG image', async () => { + const svgContent = '' + + global.fetch = mock(() => + Promise.resolve({ + ok: true, + blob: async () => { + // Use jsdom's Blob constructor for proper FileReader compatibility + const JSDOMWindow = global.window as typeof globalThis & { + Blob: typeof Blob + } + const blob = new JSDOMWindow.Blob([svgContent], { + type: 'text/plain', + }) + // Add arrayBuffer method since jsdom Blob doesn't have it + blob.arrayBuffer = async () => { + const encoder = new TextEncoder() + return encoder.encode(svgContent).buffer + } + return blob + }, + } as Response), + ) + + const result = await fetchLogoImage('https://example.com/logo.svg') + expect(result).toStartWith('data:image/svg+xml;base64,') + // Verify the base64 content can be decoded back to the original + const base64Part = result.split(',')[1] + const decoded = atob(base64Part) + expect(decoded).toBe(svgContent) + }) + + const imageTests = [ + { + bytes: PNG_MAGIC_BYTES, + type: 'PNG', + url: 'https://example.com/logo.png', + }, + { + bytes: JPEG_MAGIC_BYTES, + type: 'JPEG', + url: 'https://example.com/logo.jpg', + }, + ] + + imageTests.forEach(({ bytes, type, url }) => { + test(`should return URL for ${type} image`, async () => { + global.fetch = createMockFetch(true, bytes) + expect(await fetchLogoImage(url)).toBe(url) + }) + }) + + test('should throw error for failed fetch', async () => { + global.fetch = createMockFetch(false, undefined, 404) + await expect( + fetchLogoImage('https://example.com/not-found.png'), + ).rejects.toThrow('Failed to fetch image') + }) + + test('should throw error for unknown image type', async () => { + global.fetch = createMockFetch( + true, + new Uint8Array([0x00, 0x01, 0x02, 0x03]), + ) + await expect( + fetchLogoImage('https://example.com/unknown.bin'), + ).rejects.toThrow('Unknown image type') + }) + }) + + describe('setupBrandingLogo', () => { + test('should return empty string when no logo is configured', async () => { + setupScreenlyMock({}, { theme: 'light' }) + expect(await setupBrandingLogo()).toBe('') + }) + + const logoTests = [ + { theme: 'light' as const, url: 'https://example.com/light-logo.png' }, + { theme: 'dark' as const, url: 'https://example.com/dark-logo.png' }, + ] + + logoTests.forEach(({ theme, url }) => { + test(`should fetch ${theme} logo for ${theme} theme`, async () => { + setupMockWithLogo(theme, url) + global.fetch = createMockFetch(true, PNG_MAGIC_BYTES) + expect(await setupBrandingLogo()).toContain('example.com') + }) + }) + + test('should fallback to direct URL when CORS proxy fails', async () => { + setupMockWithLogo('light', TEST_LOGO_URL) + + let fetchCount = 0 + global.fetch = mock(() => { + fetchCount++ + return Promise.resolve( + fetchCount === 1 + ? ({ ok: false, status: 500 } as Response) + : ({ + ok: true, + blob: () => Promise.resolve(new Blob([PNG_MAGIC_BYTES])), + } as Response), + ) + }) + + expect(await setupBrandingLogo()).toBe(TEST_LOGO_URL) + }) + + test('should return empty string when all fetch attempts fail', async () => { + setupMockWithLogo('light', TEST_LOGO_URL) + global.fetch = createMockFetch(false, undefined, 404) + expect(await setupBrandingLogo()).toBe('') + }) + }) + + describe('setupBranding', () => { + const brandingSettings = { + screenly_color_accent: '#FF0000', + theme: 'light' as const, + screenly_color_light: '#00FF00', + screenly_logo_light: TEST_LOGO_URL, + } + + test('should setup complete branding with colors and logo', async () => { + setupScreenlyMock({ cors_proxy_url: PROXY_URL }, brandingSettings) + global.fetch = createMockFetch(true, PNG_MAGIC_BYTES) + + const branding = await setupBranding() + + expect(branding.colors.primary).toBe('#FF0000') + expect(branding.colors.secondary).toBe('#00FF00') + expect(branding.logoUrl).toBeDefined() + }) + + test('should setup branding without logo when fetch fails', async () => { + setupScreenlyMock({ cors_proxy_url: PROXY_URL }, brandingSettings) + global.fetch = createMockFetch(false, undefined, 404) + + const branding = await setupBranding() + + expect(branding.colors.primary).toBe('#FF0000') + expect(branding.logoUrl).toBeUndefined() }) }) }) From 5bee791086a68accd793ff670abec874966ba72f Mon Sep 17 00:00:00 2001 From: Nico Miguelino Date: Thu, 29 Jan 2026 11:16:34 -0800 Subject: [PATCH 05/25] Update edge-apps/edge-apps-library/package.json --- edge-apps/edge-apps-library/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edge-apps/edge-apps-library/package.json b/edge-apps/edge-apps-library/package.json index ea268fc3e..e009928b3 100644 --- a/edge-apps/edge-apps-library/package.json +++ b/edge-apps/edge-apps-library/package.json @@ -39,7 +39,7 @@ "build:types": "tsc", "test": "bun test --parallel", "test:unit": "bun test --parallel", - "test:coverage": "bun test --coverage --parallel --coverage-reporter=text", + "test:coverage": "bun test --coverage --parallel", "lint": "eslint . --fix", "format": "prettier --write src/ scripts/ README.md index.html", "format:check": "prettier --check src/ scripts/ README.md index.html", From 13dce38ba506873faf125c7c5d2ced9d17fd1fa8 Mon Sep 17 00:00:00 2001 From: Nico Miguelino Date: Thu, 29 Jan 2026 11:44:05 -0800 Subject: [PATCH 06/25] test(edge-apps-library): update `JPEG_MAGIC_BYTES` to have only three bytes Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- edge-apps/edge-apps-library/src/utils/theme.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index c652ec52b..9a16cbe94 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -16,7 +16,7 @@ import { setupScreenlyMock, resetScreenlyMock } from '../test/mock' const PNG_MAGIC_BYTES = new Uint8Array([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ]) -const JPEG_MAGIC_BYTES = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]) +const JPEG_MAGIC_BYTES = new Uint8Array([0xff, 0xd8, 0xff]) const DEFAULT_SECONDARY = '#adafbe' const PROXY_URL = 'https://proxy.example.com' const TEST_LOGO_URL = 'https://example.com/logo.png' From ae528e67455a56e24e58e4fb5b8b3ad033e210a4 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 11:57:26 -0800 Subject: [PATCH 07/25] fix(edge-apps-library): correct cors_proxy_url parameter handling in tests - Add corsProxyUrl parameter to createMockScreenly and setupScreenlyMock - Fix type mismatch where cors_proxy_url was incorrectly passed as metadata - Update theme tests to use correct function signature Co-Authored-By: Claude Sonnet 4.5 --- edge-apps/edge-apps-library/src/test/mock.ts | 6 ++++-- edge-apps/edge-apps-library/src/utils/theme.test.ts | 9 +++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/edge-apps/edge-apps-library/src/test/mock.ts b/edge-apps/edge-apps-library/src/test/mock.ts index b91794387..bfebb5184 100644 --- a/edge-apps/edge-apps-library/src/test/mock.ts +++ b/edge-apps/edge-apps-library/src/test/mock.ts @@ -36,12 +36,13 @@ export const mockSettings: ScreenlySettings = { export function createMockScreenly( metadata: Partial = {}, settings: Partial = {}, + corsProxyUrl = 'http://localhost:8080', ): ScreenlyObject { return { signalReadyForRendering: () => {}, metadata: { ...mockMetadata, ...metadata }, settings: { ...mockSettings, ...settings }, - cors_proxy_url: 'http://localhost:8080', + cors_proxy_url: corsProxyUrl, } } @@ -52,8 +53,9 @@ export function createMockScreenly( export function setupScreenlyMock( metadata: Partial = {}, settings: Partial = {}, + corsProxyUrl?: string, ): ScreenlyObject { - const mock = createMockScreenly(metadata, settings) + const mock = createMockScreenly(metadata, settings, corsProxyUrl) global.screenly = mock return mock } diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index 9a16cbe94..3251f185a 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -39,10 +39,7 @@ function getCSSProperty(name: string): string { function setupMockWithLogo(theme: 'light' | 'dark', logoUrl: string) { const logoKey = theme === 'light' ? 'screenly_logo_light' : 'screenly_logo_dark' - setupScreenlyMock( - { cors_proxy_url: PROXY_URL }, - { theme, [logoKey]: logoUrl }, - ) + setupScreenlyMock({}, { theme, [logoKey]: logoUrl }, PROXY_URL) } // eslint-disable-next-line max-lines-per-function @@ -317,7 +314,7 @@ describe('theme utilities', () => { } test('should setup complete branding with colors and logo', async () => { - setupScreenlyMock({ cors_proxy_url: PROXY_URL }, brandingSettings) + setupScreenlyMock({}, brandingSettings, PROXY_URL) global.fetch = createMockFetch(true, PNG_MAGIC_BYTES) const branding = await setupBranding() @@ -328,7 +325,7 @@ describe('theme utilities', () => { }) test('should setup branding without logo when fetch fails', async () => { - setupScreenlyMock({ cors_proxy_url: PROXY_URL }, brandingSettings) + setupScreenlyMock({}, brandingSettings, PROXY_URL) global.fetch = createMockFetch(false, undefined, 404) const branding = await setupBranding() From 86ab2787897ef3b1fa1801dbdb9d9ec31af888c4 Mon Sep 17 00:00:00 2001 From: Nico Miguelino Date: Thu, 29 Jan 2026 11:59:57 -0800 Subject: [PATCH 08/25] test(edge-apps-library): make the assertion for the error message less strict Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- edge-apps/edge-apps-library/src/utils/theme.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index 3251f185a..bbb30a0de 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -246,7 +246,7 @@ describe('theme utilities', () => { global.fetch = createMockFetch(false, undefined, 404) await expect( fetchLogoImage('https://example.com/not-found.png'), - ).rejects.toThrow('Failed to fetch image') + ).rejects.toThrow(/Failed to fetch image/) }) test('should throw error for unknown image type', async () => { From ead3f2afba6fc12afd7896ed1c05e75b07804528 Mon Sep 17 00:00:00 2001 From: Nico Miguelino Date: Thu, 29 Jan 2026 12:12:56 -0800 Subject: [PATCH 09/25] Update edge-apps/edge-apps-library/src/utils/theme.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- edge-apps/edge-apps-library/src/utils/theme.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index bbb30a0de..d0fdec7e3 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -215,7 +215,7 @@ describe('theme utilities', () => { ) const result = await fetchLogoImage('https://example.com/logo.svg') - expect(result).toStartWith('data:image/svg+xml;base64,') + expect(result.startsWith('data:image/svg+xml;base64,')).toBe(true) // Verify the base64 content can be decoded back to the original const base64Part = result.split(',')[1] const decoded = atob(base64Part) From 1ef9018fbd182d784fc93b866b887989e47f694a Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 12:19:13 -0800 Subject: [PATCH 10/25] refactor(edge-apps-library): simplify getPrimaryColor test descriptions - Replace complex template literal test names with static descriptions - Improve test output readability Co-Authored-By: Claude Sonnet 4.5 --- .../edge-apps-library/src/utils/theme.test.ts | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index d0fdec7e3..6aadd5e1d 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -53,29 +53,20 @@ describe('theme utilities', () => { }) describe('getPrimaryColor', () => { - const testCases = [ - { - input: undefined, - expected: DEFAULT_THEME_COLORS.primary, - desc: 'no accent color provided', - }, - { - input: '#ffffff', - expected: DEFAULT_THEME_COLORS.primary, - desc: 'accent color is white', - }, - { - input: '#FFFFFF', - expected: DEFAULT_THEME_COLORS.primary, - desc: 'accent color is white (uppercase)', - }, - { input: '#FF0000', expected: '#FF0000', desc: 'provided accent color' }, - ] + test('should return default color when no accent color provided', () => { + expect(getPrimaryColor(undefined)).toBe(DEFAULT_THEME_COLORS.primary) + }) - testCases.forEach(({ input, expected, desc }) => { - test(`should return ${desc === 'provided accent color' ? 'provided' : 'default'} color when ${desc}`, () => { - expect(getPrimaryColor(input)).toBe(expected) - }) + test('should return default color when accent color is white', () => { + expect(getPrimaryColor('#ffffff')).toBe(DEFAULT_THEME_COLORS.primary) + }) + + test('should return default color when accent color is white (uppercase)', () => { + expect(getPrimaryColor('#FFFFFF')).toBe(DEFAULT_THEME_COLORS.primary) + }) + + test('should return provided accent color', () => { + expect(getPrimaryColor('#FF0000')).toBe('#FF0000') }) }) From 9e6416b4c43d1dc8f1e25188bf32197b3c28893f Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 12:23:52 -0800 Subject: [PATCH 11/25] refactor(edge-apps-library): simplify getSecondaryColor test descriptions - Replace complex template literal test names with static descriptions - Improve test output readability Co-Authored-By: Claude Sonnet 4.5 --- .../edge-apps-library/src/utils/theme.test.ts | 64 +++++++------------ 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/edge-apps/edge-apps-library/src/utils/theme.test.ts b/edge-apps/edge-apps-library/src/utils/theme.test.ts index 6aadd5e1d..cb231c71c 100644 --- a/edge-apps/edge-apps-library/src/utils/theme.test.ts +++ b/edge-apps/edge-apps-library/src/utils/theme.test.ts @@ -71,48 +71,30 @@ describe('theme utilities', () => { }) describe('getSecondaryColor', () => { - const testCases = [ - { - theme: undefined, - light: undefined, - dark: undefined, - expected: DEFAULT_THEME_COLORS.secondary, - desc: 'theme is undefined', - }, - { - theme: 'light' as const, - light: '#123456', - dark: undefined, - expected: '#123456', - desc: 'theme is light', - }, - { - theme: 'dark' as const, - light: undefined, - dark: '#654321', - expected: '#654321', - desc: 'theme is dark', - }, - { - theme: 'light' as const, - light: '#ffffff', - dark: undefined, - expected: DEFAULT_SECONDARY, - desc: 'light color is white', - }, - { - theme: 'dark' as const, - light: undefined, - dark: '#FFFFFF', - expected: DEFAULT_SECONDARY, - desc: 'dark color is white', - }, - ] + test('should return default secondary when theme is undefined', () => { + expect(getSecondaryColor(undefined, undefined, undefined)).toBe( + DEFAULT_THEME_COLORS.secondary, + ) + }) - testCases.forEach(({ theme, light, dark, expected, desc }) => { - test(`should return ${desc.includes('white') ? 'default' : light || dark || 'default secondary'} when ${desc}`, () => { - expect(getSecondaryColor(theme, light, dark)).toBe(expected) - }) + test('should return light color when theme is light', () => { + expect(getSecondaryColor('light', '#123456', undefined)).toBe('#123456') + }) + + test('should return dark color when theme is dark', () => { + expect(getSecondaryColor('dark', undefined, '#654321')).toBe('#654321') + }) + + test('should return default secondary when light color is white', () => { + expect(getSecondaryColor('light', '#ffffff', undefined)).toBe( + DEFAULT_SECONDARY, + ) + }) + + test('should return default secondary when dark color is white', () => { + expect(getSecondaryColor('dark', undefined, '#FFFFFF')).toBe( + DEFAULT_SECONDARY, + ) }) }) From 2780971c11a29222b0e3c11e62b412a1d8a37c49 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 14:04:18 -0800 Subject: [PATCH 12/25] feat(ci): add coverage summary to GitHub Actions step summary - Parse lcov.info to extract line and function coverage - Display coverage metrics in GitHub Actions UI - Configure bunfig.toml to output both text and lcov formats Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 36 +++++++++++++++++++++++++ edge-apps/edge-apps-library/bunfig.toml | 1 + 2 files changed, 37 insertions(+) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index 024fb997f..6c22094a0 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -161,6 +161,42 @@ jobs: working-directory: edge-apps/${{ matrix.app }} run: bun run test:unit + - name: Generate coverage summary + if: always() + working-directory: edge-apps/${{ matrix.app }} + run: | + if [ -f "coverage/lcov.info" ]; then + echo "## Code Coverage Report for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + # Parse lcov.info and generate summary + awk ' + BEGIN { + total_lines = 0 + covered_lines = 0 + total_funcs = 0 + covered_funcs = 0 + } + /^LF:/ { total_lines += $2 } + /^LH:/ { covered_lines += $2 } + /^FNF:/ { total_funcs += $2 } + /^FNH:/ { covered_funcs += $2 } + END { + line_pct = (total_lines > 0) ? (covered_lines / total_lines * 100) : 0 + func_pct = (total_funcs > 0) ? (covered_funcs / total_funcs * 100) : 0 + + printf "| Metric | Coverage | Covered | Total |\n" + printf "|--------|----------|---------|-------|\n" + printf "| Lines | %.2f%% | %d | %d |\n", line_pct, covered_lines, total_lines + printf "| Functions | %.2f%% | %d | %d |\n", func_pct, covered_funcs, total_funcs + } + ' FS=: coverage/lcov.info >> "$GITHUB_STEP_SUMMARY" + + echo "" >> "$GITHUB_STEP_SUMMARY" + else + echo "No coverage data found for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" + fi + - name: Build application working-directory: edge-apps/${{ matrix.app }} run: bun run build diff --git a/edge-apps/edge-apps-library/bunfig.toml b/edge-apps/edge-apps-library/bunfig.toml index 6c7b562de..3ff4ebccf 100644 --- a/edge-apps/edge-apps-library/bunfig.toml +++ b/edge-apps/edge-apps-library/bunfig.toml @@ -2,4 +2,5 @@ preload = ["./src/test/setup.ts"] coverage = true coverageThreshold = 0.9 +coverageReporter = ["text", "lcov"] From fbe8a69f4cf980942a6d29f7a9136cf271439761 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 14:11:53 -0800 Subject: [PATCH 13/25] feat(ci): add per-file coverage details to summary - Display coverage for each source file in markdown table - Show function and line coverage percentages per file - Include totals row for overall coverage Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 42 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index 6c22094a0..e5e2f0746 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -168,29 +168,39 @@ jobs: if [ -f "coverage/lcov.info" ]; then echo "## Code Coverage Report for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| File | % Funcs | % Lines |" >> "$GITHUB_STEP_SUMMARY" + echo "|------|---------|---------|" >> "$GITHUB_STEP_SUMMARY" - # Parse lcov.info and generate summary - awk ' - BEGIN { + # Parse lcov.info and generate per-file summary + awk 'BEGIN { total_lines = 0 covered_lines = 0 total_funcs = 0 covered_funcs = 0 } - /^LF:/ { total_lines += $2 } - /^LH:/ { covered_lines += $2 } - /^FNF:/ { total_funcs += $2 } - /^FNH:/ { covered_funcs += $2 } - END { - line_pct = (total_lines > 0) ? (covered_lines / total_lines * 100) : 0 - func_pct = (total_funcs > 0) ? (covered_funcs / total_funcs * 100) : 0 - - printf "| Metric | Coverage | Covered | Total |\n" - printf "|--------|----------|---------|-------|\n" - printf "| Lines | %.2f%% | %d | %d |\n", line_pct, covered_lines, total_lines - printf "| Functions | %.2f%% | %d | %d |\n", func_pct, covered_funcs, total_funcs + /^SF:/ { + if (length(current_file) > 0) { + file_line_pct = (file_lf > 0) ? (file_lh / file_lf * 100) : 0 + file_func_pct = (file_fnf > 0) ? (file_fnh / file_fnf * 100) : 0 + printf "| %s | %.2f%% | %.2f%% |\n", current_file, file_func_pct, file_line_pct + } + current_file = substr($0, 4) + file_lf = 0; file_lh = 0; file_fnf = 0; file_fnh = 0 } - ' FS=: coverage/lcov.info >> "$GITHUB_STEP_SUMMARY" + /^LF:/ { file_lf = $2; total_lines += $2 } + /^LH:/ { file_lh = $2; covered_lines += $2 } + /^FNF:/ { file_fnf = $2; total_funcs += $2 } + /^FNH:/ { file_fnh = $2; covered_funcs += $2 } + END { + if (length(current_file) > 0) { + file_line_pct = (file_lf > 0) ? (file_lh / file_lf * 100) : 0 + file_func_pct = (file_fnf > 0) ? (file_fnh / file_fnf * 100) : 0 + printf "| %s | %.2f%% | %.2f%% |\n", current_file, file_func_pct, file_line_pct + } + total_line_pct = (total_lines > 0) ? (covered_lines / total_lines * 100) : 0 + total_func_pct = (total_funcs > 0) ? (covered_funcs / total_funcs * 100) : 0 + printf "| **All files** | **%.2f%%** | **%.2f%%** |\n", total_func_pct, total_line_pct + }' FS=: coverage/lcov.info >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" else From f17e9eef07655ab952395ccc5dae5142e99ecd8d Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 14:39:15 -0800 Subject: [PATCH 14/25] fix(ci): parse Bun test output for accurate coverage numbers - Parse Bun's text output instead of lcov.info for coverage summary - Capture test output with tee for parsing - Ensure coverage numbers match terminal output exactly Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 64 +++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index e5e2f0746..626697d24 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -157,54 +157,54 @@ jobs: working-directory: edge-apps/${{ matrix.app }} run: bun run format:check + - name: Clean coverage directory + working-directory: edge-apps/${{ matrix.app }} + run: rm -rf coverage + - name: Run unit tests + id: run-tests working-directory: edge-apps/${{ matrix.app }} - run: bun run test:unit + run: bun run test:unit 2>&1 | tee test-output.txt - name: Generate coverage summary - if: always() + if: success() working-directory: edge-apps/${{ matrix.app }} run: | - if [ -f "coverage/lcov.info" ]; then + if [ -f "test-output.txt" ]; then echo "## Code Coverage Report for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| File | % Funcs | % Lines |" >> "$GITHUB_STEP_SUMMARY" echo "|------|---------|---------|" >> "$GITHUB_STEP_SUMMARY" - # Parse lcov.info and generate per-file summary - awk 'BEGIN { - total_lines = 0 - covered_lines = 0 - total_funcs = 0 - covered_funcs = 0 + # Parse bun test output for coverage table + awk ' + BEGIN { in_table = 0 } + /^-+\|/ { + in_table = 1 + next + } + in_table && /^All files/ { + split($0, parts, "|") + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[2]) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[3]) + printf "| **All files** | **%s%%** | **%s%%** |\n", parts[2], parts[3] + next + } + in_table && /^ src\// { + split($0, parts, "|") + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[1]) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[2]) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[3]) + printf "| %s | %s%% | %s%% |\n", parts[1], parts[2], parts[3] } - /^SF:/ { - if (length(current_file) > 0) { - file_line_pct = (file_lf > 0) ? (file_lh / file_lf * 100) : 0 - file_func_pct = (file_fnf > 0) ? (file_fnh / file_fnf * 100) : 0 - printf "| %s | %.2f%% | %.2f%% |\n", current_file, file_func_pct, file_line_pct - } - current_file = substr($0, 4) - file_lf = 0; file_lh = 0; file_fnf = 0; file_fnh = 0 + /^-+\|.*-+$/ && in_table { + in_table = 0 } - /^LF:/ { file_lf = $2; total_lines += $2 } - /^LH:/ { file_lh = $2; covered_lines += $2 } - /^FNF:/ { file_fnf = $2; total_funcs += $2 } - /^FNH:/ { file_fnh = $2; covered_funcs += $2 } - END { - if (length(current_file) > 0) { - file_line_pct = (file_lf > 0) ? (file_lh / file_lf * 100) : 0 - file_func_pct = (file_fnf > 0) ? (file_fnh / file_fnf * 100) : 0 - printf "| %s | %.2f%% | %.2f%% |\n", current_file, file_func_pct, file_line_pct - } - total_line_pct = (total_lines > 0) ? (covered_lines / total_lines * 100) : 0 - total_func_pct = (total_funcs > 0) ? (covered_funcs / total_funcs * 100) : 0 - printf "| **All files** | **%.2f%%** | **%.2f%%** |\n", total_func_pct, total_line_pct - }' FS=: coverage/lcov.info >> "$GITHUB_STEP_SUMMARY" + ' test-output.txt >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" else - echo "No coverage data found for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" + echo "No test output found for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" fi - name: Build application From 060254150c42ff6daa1e15ed6777184c717de566 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 15:22:41 -0800 Subject: [PATCH 15/25] style(ci): right-align percentage columns in coverage table - Left-align file names for readability - Right-align percentage columns for easier comparison Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index 626697d24..ce698b6b4 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -174,7 +174,7 @@ jobs: echo "## Code Coverage Report for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "| File | % Funcs | % Lines |" >> "$GITHUB_STEP_SUMMARY" - echo "|------|---------|---------|" >> "$GITHUB_STEP_SUMMARY" + echo "|:-----|--------:|--------:|" >> "$GITHUB_STEP_SUMMARY" # Parse bun test output for coverage table awk ' From 30850987e342763398b6ad1e9c08dd3230a1322f Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 15:29:50 -0800 Subject: [PATCH 16/25] fix(ci): use grouped commands for shellcheck compliance - Replace individual redirects with command block - Resolves SC2129 shellcheck warning Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/edge-app-checks.yml | 66 ++++++++++++++------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/.github/workflows/edge-app-checks.yml b/.github/workflows/edge-app-checks.yml index ce698b6b4..d088e5861 100644 --- a/.github/workflows/edge-app-checks.yml +++ b/.github/workflows/edge-app-checks.yml @@ -171,38 +171,40 @@ jobs: working-directory: edge-apps/${{ matrix.app }} run: | if [ -f "test-output.txt" ]; then - echo "## Code Coverage Report for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "| File | % Funcs | % Lines |" >> "$GITHUB_STEP_SUMMARY" - echo "|:-----|--------:|--------:|" >> "$GITHUB_STEP_SUMMARY" - - # Parse bun test output for coverage table - awk ' - BEGIN { in_table = 0 } - /^-+\|/ { - in_table = 1 - next - } - in_table && /^All files/ { - split($0, parts, "|") - gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[2]) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[3]) - printf "| **All files** | **%s%%** | **%s%%** |\n", parts[2], parts[3] - next - } - in_table && /^ src\// { - split($0, parts, "|") - gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[1]) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[2]) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[3]) - printf "| %s | %s%% | %s%% |\n", parts[1], parts[2], parts[3] - } - /^-+\|.*-+$/ && in_table { - in_table = 0 - } - ' test-output.txt >> "$GITHUB_STEP_SUMMARY" - - echo "" >> "$GITHUB_STEP_SUMMARY" + { + echo "## Code Coverage Report for ${{ matrix.app }}" + echo "" + echo "| File | % Funcs | % Lines |" + echo "|:-----|--------:|--------:|" + + # Parse bun test output for coverage table + awk ' + BEGIN { in_table = 0 } + /^-+\|/ { + in_table = 1 + next + } + in_table && /^All files/ { + split($0, parts, "|") + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[2]) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[3]) + printf "| **All files** | **%s%%** | **%s%%** |\n", parts[2], parts[3] + next + } + in_table && /^ src\// { + split($0, parts, "|") + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[1]) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[2]) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[3]) + printf "| %s | %s%% | %s%% |\n", parts[1], parts[2], parts[3] + } + /^-+\|.*-+$/ && in_table { + in_table = 0 + } + ' test-output.txt + + echo "" + } >> "$GITHUB_STEP_SUMMARY" else echo "No test output found for ${{ matrix.app }}" >> "$GITHUB_STEP_SUMMARY" fi From 6bdc9320fdd969c7b10c84a27c22ca8674f45c0a Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Thu, 29 Jan 2026 15:59:47 -0800 Subject: [PATCH 17/25] temporary: check if the code coverage pipeline will not break existing workflows --- edge-apps/blueprint/ts/components/TopBar.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/edge-apps/blueprint/ts/components/TopBar.vue b/edge-apps/blueprint/ts/components/TopBar.vue index edc65835f..56550d35a 100644 --- a/edge-apps/blueprint/ts/components/TopBar.vue +++ b/edge-apps/blueprint/ts/components/TopBar.vue @@ -1,4 +1,5 @@