Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 63 additions & 0 deletions packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from 'vitest';
import * as exportUtils from './exportGraphData';

describe('exportGraphData', () => {
it('generates a human-friendly default filename for graph exports', () => {
vi.useFakeTimers();
// 2026-02-14T20:27:20.000Z
vi.setSystemTime(new Date('2026-02-14T20:27:20.000Z'));

const name = exportUtils.getDefaultGraphExportFileName();

expect(name).toBe('bh-graph-2026-02-14_20-27-20.json');

vi.useRealTimers();
});

it('uses the default filename generator when exporting JSON', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-02-14T20:27:20.000Z'));

const originalCreateObjectURL = (window.URL as any).createObjectURL;
const createObjectUrlSpy = vi.fn(() => 'blob:mock');
Object.defineProperty(window.URL, 'createObjectURL', {
value: createObjectUrlSpy,
writable: true,
});

let createdAnchor: HTMLAnchorElement | undefined;
const originalCreateElement = document.createElement.bind(document);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: any, options?: any) => {
const el = originalCreateElement(tagName, options) as any;
if (tagName === 'a') createdAnchor = el as HTMLAnchorElement;
return el;
});

const originalMouseEvent = globalThis.MouseEvent;
class MockMouseEvent extends Event {
constructor(type: string, _init?: any) {
super(type);
}
}
Object.defineProperty(globalThis, 'MouseEvent', {
value: MockMouseEvent,
writable: true,
});

exportUtils.exportToJson({ a: 1 });

expect(createdAnchor?.download).toBe('bh-graph-2026-02-14_20-27-20.json');
expect(createObjectUrlSpy).toHaveBeenCalled();

createElementSpy.mockRestore();
Object.defineProperty(window.URL, 'createObjectURL', {
value: originalCreateObjectURL,
writable: true,
});
Object.defineProperty(globalThis, 'MouseEvent', {
value: originalMouseEvent,
writable: true,
});
vi.useRealTimers();
});
});
10 changes: 9 additions & 1 deletion packages/javascript/bh-shared-ui/src/utils/exportGraphData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ export const downloadFile = ({ data, fileName, fileType }: { data: any; fileName
a.remove();
};

export const getDefaultGraphExportFileName = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

We use luxon internally to handle datetime related logic since that tends to be cumbersome to do without a package. I would recommend reaching for that here so that the formatting is more ergonomic.

Suggested change
export const getDefaultGraphExportFileName = () => {
export const getDefaultGraphExportFileName = () => {
const formattedDate = DateTime.now().toFormat(LuxonFormat.DATETIME_WITHOUT_TIMEZONE)
return `bh-graph-${formattedDate}.json`;
};

There are some other formats to choose from if you have a preference.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will get this updated when I get a chance!

const iso = new Date().toISOString();
// iso: YYYY-MM-DDTHH:mm:ss.sssZ
const withoutMs = iso.replace(/\.\d{3}Z$/, '');
const safe = withoutMs.replace('T', '_').replace(/:/g, '-');
return `bh-graph-${safe}.json`;
};

export const exportToJson = (data: any) => {
downloadFile({
data: JSON.stringify(data),
fileName: 'bh-graph.json',
fileName: getDefaultGraphExportFileName(),
fileType: 'text/json',
});
};
Loading