Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
43af3e9
test(vcr): assert telemetry stub exports match activeSpans
roryabraham Jun 17, 2026
8a4c9de
fix(vcr): complete telemetry-activeSpans stub exports
roryabraham Jun 17, 2026
2c235f3
ci(vcr): add PR workflow for victory-chart-renderer tests
roryabraham Jun 17, 2026
508eb39
fix(vcr): stub headless runtime deps for standalone CLI
roryabraham Jun 17, 2026
7003b31
ci(vcr): include Localize paths in PR workflow filter
roryabraham Jun 17, 2026
dfedaa1
ci(vcr): use pull_request.paths instead of dorny
roryabraham Jun 17, 2026
4f6f818
ci(vcr): broaden workflow paths filter to src/**
roryabraham Jun 17, 2026
3557b1f
fix(vcr): use real Onyx with MemoryOnlyProvider
roryabraham Jun 17, 2026
0fb51ad
fix(vcr): unwrap Onyx CJS exports for Bun compile
roryabraham Jun 17, 2026
9b7058d
style: run prettier on VCR server files
roryabraham Jun 17, 2026
3f280da
style(vcr): fix no-inline-named-export in stubs
roryabraham Jun 17, 2026
c02ad74
fix(vcr): drop bun types from VCR tsconfig
roryabraham Jun 17, 2026
2c87a47
fix(vcr): exit CLI after render to unblock smoke tests
roryabraham Jun 17, 2026
2878b23
ci: replace VCR workflow with generic bunTests
roryabraham Jun 17, 2026
ad77018
ci(bun): include VCR golden render tests in test:bun
roryabraham Jun 17, 2026
f753496
fix(vcr): use textSupporting color in cartesian fixtures
roryabraham Jun 17, 2026
8e39e5b
chore(vcr): use default bun test discovery in workspace
roryabraham Jun 17, 2026
20cffa1
test(bun): add describe blocks and run tests concurrently
roryabraham Jun 17, 2026
f39329d
ci(bun): run test:bun script in workflow
roryabraham Jun 17, 2026
715b815
chore(vcr): gitignore local test render output directory
roryabraham Jun 17, 2026
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
44 changes: 44 additions & 0 deletions .github/workflows/bunTests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Bun tests

on:
pull_request:
types: [opened, synchronize]
branches-ignore: [staging, production]
paths:
- 'server/**'
- 'src/**'
- 'bunfig.toml'
- '.bun-version'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/bunTests.yml'

concurrency:
group: ${{ github.ref }}-bun-tests
cancel-in-progress: true

jobs:
test:
name: Bun tests
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout
uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1

- name: Setup Node
uses: ./.github/actions/composite/setupNode

- name: Setup Bun
# v2.2.0
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version-file: .bun-version

- name: Run Bun tests
run: npm run test:bun

- name: Build victory-chart-renderer Linux x64 binary
run: npm run server:vcr:build:linux

- name: Smoke-test compiled victory-chart-renderer binary
run: ./server/victory-chart-renderer/dist/victory-chart-renderer-linux-x64 --chart-xml "$(cat server/victory-chart-renderer/tests/fixtures/top-categories-6.xml)" --out /tmp/vcr-smoke.png && test -s /tmp/vcr-smoke.png
10 changes: 10 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Bun test discovery for server-side tooling (Jest owns src/ and tests/).
[test]
root = "server"
pathIgnorePatterns = [
"**/fixtures/**",
"**/dist/**",
"**/.dev/**",
"**/__output__/**",
"**/__golden__/**",
]
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"compress-svg": "npx ts-node scripts/compressSvg.ts --dir assets/images && npx ts-node scripts/compressSvg.ts --dir docs/assets/images",
"server:vcr:dev": "npm run dev -w @expensify/victory-chart-renderer --",
"server:vcr:test": "npm run test -w @expensify/victory-chart-renderer --",
"test:bun": "bun test --concurrent --max-concurrency 7",
"server:vcr:build:linux": "npm run build:linux -w @expensify/victory-chart-renderer --",
"server:vcr:build:linux-arm": "npm run build:linux-arm -w @expensify/victory-chart-renderer --",
"server:vcr:build:macos": "npm run build:macos -w @expensify/victory-chart-renderer --"
Expand Down
9 changes: 9 additions & 0 deletions server/libs/getCjsDefaultExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** Unwrap Bun's __toESM(require(), 1) double-default when importing CJS modules. */
function getCjsDefaultExport(namespace: Record<string, unknown>): unknown {
const first = namespace.default ?? namespace;
const second = typeof first === 'object' && first !== null && 'default' in first ? ((first as Record<string, unknown>).default ?? first) : first;

return second;
}

export default getCjsDefaultExport;
18 changes: 17 additions & 1 deletion server/plugins/rnStubPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function createRnStubPlugin(stubRoot: string): BunPlugin {
}));

build.onResolve({filter: /^@react-native-community\/netinfo$/}, () => ({
path: resolve(stubRoot, 'react-native/subpath.ts'),
path: resolve(stubRoot, 'react-native-community-netinfo.ts'),
}));

build.onResolve({filter: /^react-native$/}, () => ({
Expand All @@ -57,6 +57,22 @@ export default function createRnStubPlugin(stubRoot: string): BunPlugin {
build.onResolve({filter: /^react-native-nitro-fetch$/}, () => ({
path: resolve(stubRoot, 'react-native-nitro-fetch.ts'),
}));

build.onResolve({filter: /^react-native-onyx$/}, () => ({
path: resolve(stubRoot, 'react-native-onyx.ts'),
}));

build.onResolve({filter: /^\.\/platforms$/}, (args) => {
if (!args.importer.includes('react-native-onyx/dist/storage')) {
return;
}

return {path: resolve(stubRoot, 'onyx-storage-platform.ts')};
});

build.onResolve({filter: /^react-native-localize$/}, () => ({
path: resolve(stubRoot, 'react-native-localize.ts'),
}));
},
};
}
4 changes: 4 additions & 0 deletions server/stubs/onyx-storage-platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import getCjsDefaultExport from '@server/libs/getCjsDefaultExport';
import * as MemoryOnlyProviderNamespace from 'react-native-onyx/dist/storage/providers/MemoryOnlyProvider';

export default getCjsDefaultExport(MemoryOnlyProviderNamespace);
22 changes: 22 additions & 0 deletions server/stubs/react-native-community-netinfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const NetInfo = {
configure: () => {},
addEventListener: (callback: (state: {isConnected: boolean; isInternetReachable: boolean; type: string}) => void) => {
queueMicrotask(() => {
callback({
isConnected: true,
isInternetReachable: true,
type: 'wifi',
});
});
return () => {};
},
refresh: () => Promise.resolve(),
fetch: () =>
Promise.resolve({
isConnected: true,
isInternetReachable: true,
type: 'wifi',
}),
};

export default NetInfo;
22 changes: 22 additions & 0 deletions server/stubs/react-native-localize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function findBestLanguageTag() {
return {languageTag: 'en-US', isRTL: false};
}

const RNLocalize = {
getLocales: () => [{languageCode: 'en', countryCode: 'US', languageTag: 'en-US', isRTL: false}],
getNumberFormatSettings: () => ({
decimalSeparator: '.',
groupingSeparator: ',',
}),
getCalendar: () => 'gregorian',
getCountry: () => 'US',
getCurrencies: () => ['USD'],
getTemperatureUnit: () => 'celsius',
getTimeZone: () => 'America/Los_Angeles',
uses24HourClock: () => false,
usesMetricSystem: () => false,
findBestLanguageTag,
};

export {findBestLanguageTag};
export default RNLocalize;
16 changes: 16 additions & 0 deletions server/stubs/react-native-onyx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import getCjsDefaultExport from '@server/libs/getCjsDefaultExport';
import * as OnyxNamespace from 'react-native-onyx/dist/Onyx';
import * as useOnyxNamespace from 'react-native-onyx/dist/useOnyx';

/*
* ESM entry for react-native-onyx in the victory-chart-renderer bundle.
*
* Bun compile cannot reliably use the package's CJS index.js re-export of the default
* export, so importers should keep using `react-native-onyx` while this file is wired
* in via rnStubPlugin.
*/
const Onyx = getCjsDefaultExport(OnyxNamespace);
const useOnyx = getCjsDefaultExport(useOnyxNamespace);

export {useOnyx};
export default Onyx;
25 changes: 23 additions & 2 deletions server/stubs/sentry-react-native.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
const noopSpan = {
setAttribute: () => {},
setAttributes: () => {},
setStatus: () => {},
end: () => {},
};

function startInactiveSpan() {
return noopSpan;
}

function spanToJSON() {
return {data: {}};
}

const logger = {
warn: () => {},
};

const Sentry = {
startInactiveSpan: () => undefined,
spanToJSON: () => ({data: {}}),
startInactiveSpan,
spanToJSON,
logger,
};

export {startInactiveSpan, spanToJSON, logger};
export default Sentry;
23 changes: 21 additions & 2 deletions server/stubs/telemetry-activeSpans.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
const noopSpan = {
setAttribute: () => {},
setAttributes: () => {},
setStatus: () => {},
end: () => {},
};

function startSpan() {
return undefined;
return noopSpan;
}

function endSpan() {}

export {endSpan, startSpan};
function endSpanWithAttributes() {}

function getSpan() {
return undefined;
}

function cancelSpan() {}

function cancelAllSpans() {}

function cancelSpansByPrefix() {}

export {startSpan, endSpan, endSpanWithAttributes, getSpan, cancelSpan, cancelAllSpans, cancelSpansByPrefix};
1 change: 1 addition & 0 deletions server/victory-chart-renderer/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.dev/
dist/
tests/__output__/
2 changes: 1 addition & 1 deletion server/victory-chart-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "Headless Bun CLI that renders Expensify chart XML to PNG using victory-native and Skia.",
"scripts": {
"dev": "bun run scripts/dev.ts",
"test": "bun test",
"test": "bun test --concurrent --max-concurrency 7",
"build:linux": "bun run scripts/build.ts --target bun-linux-x64 --outfile dist/victory-chart-renderer-linux-x64",
"build:linux-arm": "bun run scripts/build.ts --target bun-linux-arm64 --outfile dist/victory-chart-renderer-linux-arm64",
"build:macos": "bun run scripts/build.ts --target bun-darwin-arm64 --outfile dist/victory-chart-renderer-darwin-arm64"
Expand Down
1 change: 1 addition & 0 deletions server/victory-chart-renderer/src/bootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import initSkiaForCli from './initSkiaForCli';
// The dynamic import ensures initSkiaForCli() runs first — static imports in cli.tsx
// would otherwise be hoisted above any top-level await in this file.
await initSkiaForCli();
await import('./initOnyxForCli');
await import('./cli');
20 changes: 14 additions & 6 deletions server/victory-chart-renderer/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ const cli = new CLI({
},
});

const xmlString = cli.namedArgs['chart-xml'];
const outPath = cli.namedArgs.out;
const tnode = parseChartXml(xmlString);
const canvasSize = resolveCanvasSize(tnode);
const fonts = await loadChartFontsForCli();
try {
const xmlString = cli.namedArgs['chart-xml'];
const outPath = cli.namedArgs.out;
const tnode = parseChartXml(xmlString);
const canvasSize = resolveCanvasSize(tnode);
const fonts = await loadChartFontsForCli();

await renderChartToPng(tnode, fonts, canvasSize, outPath);
await renderChartToPng(tnode, fonts, canvasSize, outPath);

// Onyx and network modules register listeners/timers during init; exit explicitly so CI smoke tests do not hang.
process.exit(0);
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
9 changes: 9 additions & 0 deletions server/victory-chart-renderer/src/initOnyxForCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';

Onyx.init({
keys: ONYXKEYS,
enableDevTools: false,
shouldSyncMultipleInstances: false,
ramOnlyKeys: [ONYXKEYS.RAM_ONLY_ARE_TRANSLATIONS_LOADING],
});
12 changes: 9 additions & 3 deletions server/victory-chart-renderer/src/types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,16 @@ type BunTestMatchers = {
};

declare module 'bun:test' {
function test(label: string, fn: () => void | Promise<void>): void;
function test(label: string, options: {timeout?: number}, fn: () => void | Promise<void>): void;
type BunTestFn = {
(label: string, fn: () => void | Promise<void>): void;
(label: string, options: {timeout?: number; retry?: number}, fn: () => void | Promise<void>): void;
concurrent: BunTestFn;
serial: BunTestFn;
};

const test: BunTestFn;
function describe(label: string, fn: () => void): void;
function beforeAll(fn: () => void | Promise<void>): void;
function beforeAll(fn: () => void | Promise<void>, timeout?: number): void;
function beforeEach(fn: () => void | Promise<void>): void;
function afterAll(fn: () => void | Promise<void>): void;
function afterEach(fn: () => void | Promise<void>): void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<victorychart width="680" height="430" domain="{x: [0.5, 12.5], y: [0, 2000]}" domainpadding="{x: 18, y: 0}" padding="{top: 126, bottom: 108, left: 96, right: 32}" style="{parent: {backgroundColor: '#F7F2EF', borderRadius: 16}}">
<victorylabel x="32" y="40" text="Monthly spend" style="{fill: '#002E22', fontSize: 17, fontWeight: 700, fontFamily: 'Expensify Neue'}" />
<victorylabel x="32" y="62" text="As of: May 6, 12:49 PM PT" style="{fill: '#73857E', fontSize: 11, fontWeight: 400, fontFamily: 'Expensify Neue'}" />
<victoryaxis tickvalues="[1,2,3,4,5,6,7,8,9,10,11,12]" tickformat="['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']" style="{axis: {stroke: '#E6E1DA', strokeWidth: 1}, ticks: {stroke: 'transparent'}, tickLabels: {fill: '#73857E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 16}}" />
<victoryaxis dependentaxis tickvalues="[0,500,1000,1500,2000]" tickformat="['$0','$500','$1,000','$1,500','$2,000']" style="{axis: {stroke: 'transparent'}, ticks: {stroke: 'transparent'}, grid: {stroke: '#E6E1DA', strokeWidth: 1}, tickLabels: {fill: '#73857E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 28}}" />
<victorylabel x="32" y="62" text="As of: May 6, 12:49 PM PT" style="{fill: '#76847E', fontSize: 11, fontWeight: 400, fontFamily: 'Expensify Neue'}" />
<victoryaxis tickvalues="[1,2,3,4,5,6,7,8,9,10,11,12]" tickformat="['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']" style="{axis: {stroke: '#E6E1DA', strokeWidth: 1}, ticks: {stroke: 'transparent'}, tickLabels: {fill: '#76847E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 16}}" />
<victoryaxis dependentaxis tickvalues="[0,500,1000,1500,2000]" tickformat="['$0','$500','$1,000','$1,500','$2,000']" style="{axis: {stroke: 'transparent'}, ticks: {stroke: 'transparent'}, grid: {stroke: '#E6E1DA', strokeWidth: 1}, tickLabels: {fill: '#76847E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 28}}" />
<victorybar barwidth="11" cornerradius="{top: 6, bottom: 6}" style="{data: {fill: '#1E90F2'}}" data="[{x: 1.13, y: 1080},{x: 2.13, y: 1225},{x: 3.13, y: 1375},{x: 4.13, y: 1600},{x: 5.13, y: 1630},{x: 6.13, y: 1725},{x: 7.13, y: 1400},{x: 8.13, y: 1685},{x: 9.13, y: 1725},{x: 10.13, y: 1800},{x: 11.13, y: 1800},{x: 12.13, y: 1800}]" />
<victorybar barwidth="11" cornerradius="{top: 6, bottom: 6}" style="{data: {fill: '#13C96B'}}" data="[{x: 0.87, y: 1200},{x: 1.87, y: 1320},{x: 2.87, y: 1570}]" />
<victorybar barwidth="11" cornerradius="{top: 6, bottom: 6}" style="{data: {fill: '#FF6A00'}}" data="[{x: 3.87, y: 1820}]" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<victorychart horizontal width="680" height="464" domain="{y: [0, 18000]}" domainpadding="{x: 44, y: 16}" padding="{top: 92, bottom: 84, left: 150, right: 32}" categories="{x: ['Ethan Brooks','Sofia Ramirez','Michelina Della Donna','Priya Patel','Alex Chen']}" style="{parent: {backgroundColor: '#F7F2EF', borderRadius: 16}}">
<victorylabel x="32" y="40" text="Top employees by spend" style="{fill: '#002E22', fontSize: 17, fontWeight: 700, fontFamily: 'Expensify Neue'}" />
<victorylabel x="32" y="62" text="As of: May 6, 12:49 PM PT" style="{fill: '#73857E', fontSize: 11, fontWeight: 400, fontFamily: 'Expensify Neue'}" />
<victoryaxis tickformat="['Ethan Brooks','Sofia Ramirez','Michelina Della Donna','Priya Patel','Alex Chen']" style="{axis: {stroke: 'transparent'}, ticks: {stroke: 'transparent'}, grid: {stroke: 'transparent'}, tickLabels: {fill: '#73857E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 24}}" />
<victoryaxis dependentaxis tickvalues="[0,3000,6000,9000,12000,15000,18000]" tickformat="['$0','$3,000','$6,000','$9,000','$12,000','$15,000','$18,000']" style="{axis: {stroke: 'transparent'}, ticks: {stroke: 'transparent'}, grid: {stroke: '#E6E1DA', strokeWidth: 1}, tickLabels: {fill: '#73857E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 8}}" />
<victorylabel x="32" y="62" text="As of: May 6, 12:49 PM PT" style="{fill: '#76847E', fontSize: 11, fontWeight: 400, fontFamily: 'Expensify Neue'}" />
<victoryaxis tickformat="['Ethan Brooks','Sofia Ramirez','Michelina Della Donna','Priya Patel','Alex Chen']" style="{axis: {stroke: 'transparent'}, ticks: {stroke: 'transparent'}, grid: {stroke: 'transparent'}, tickLabels: {fill: '#76847E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 24}}" />
<victoryaxis dependentaxis tickvalues="[0,3000,6000,9000,12000,15000,18000]" tickformat="['$0','$3,000','$6,000','$9,000','$12,000','$15,000','$18,000']" style="{axis: {stroke: 'transparent'}, ticks: {stroke: 'transparent'}, grid: {stroke: '#E6E1DA', strokeWidth: 1}, tickLabels: {fill: '#76847E', fontSize: 11, fontWeight: 500, fontFamily: 'Expensify Neue', padding: 8}}" />
<victorygroup offset="18">
<victorybar barwidth="16" cornerradius="{top: 8, bottom: 0}" style="{data: {fill: '#13C96B'}}" data="[{x: 'Ethan Brooks', y: 9450},{x: 'Sofia Ramirez', y: 10500},{x: 'Michelina Della Donna', y: 11900},{x: 'Priya Patel', y: 13400},{x: 'Alex Chen', y: 14300}]" />
<victorybar barwidth="16" cornerradius="{top: 8, bottom: 0}" style="{data: {fill: '#1E90F2'}}" data="[{x: 'Ethan Brooks', y: 8800},{x: 'Sofia Ramirez', y: 10250},{x: 'Michelina Della Donna', y: 11250},{x: 'Priya Patel', y: 13900},{x: 'Alex Chen', y: 12500}]" />
Expand Down
Loading
Loading