Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/shared/src/components/CalendarHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function getRange(count: number): number[] {
return Array.from(new Array(Math.max(0, count)), (_, i) => i);
}

function getBins(values: number[]): number[] {
export function getBins(values: number[]): number[] {
const uniques = Array.from(new Set(values)).sort((a, b) => a - b);
if (uniques.length <= BINS) {
return [
Expand All @@ -66,7 +66,7 @@ function getBins(values: number[]): number[] {
);
}

function getBin(value: number, bins: number[]): number {
export function getBin(value: number, bins: number[]): number {
if (!value) {
return 0;
}
Expand Down
99 changes: 61 additions & 38 deletions packages/shared/src/components/imageShare/SnapshotButton.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const mockDisplayToast = jest.fn();

jest.mock('../../lib/imageShare/captureShareImage', () => ({
captureShareImage: (...args: unknown[]) => mockCapture(...args),
SHARE_IMAGE_WIDTH: 1200,
SHARE_IMAGE_HEIGHT: 630,
}));

jest.mock('../../lib/imageShare/copyShareImage', () => ({
Expand All @@ -19,44 +21,65 @@ jest.mock('../../lib/imageShare/downloadShareImage', () => ({
downloadShareImage: (...args: unknown[]) => mockDownload(...args),
}));

jest.mock('../../features/snapshot/shutterSound', () => ({
playShutterSound: jest.fn(),
}));

jest.mock('../../hooks/useToastNotification', () => ({
useToastNotification: () => ({ displayToast: mockDisplayToast }),
ToastType: { Success: 'success', Error: 'error' },
}));

jest.mock('../../features/snapshot/shutterSound', () => ({
playShutterSound: jest.fn(),
}));

jest.mock('../../hooks/useRequestProtocol', () => ({
useRequestProtocol: () => ({ isCompanion: false }),
}));

const blob = new Blob(['png'], { type: 'image/png' });

const renderComponent = (props = {}) => {
const target = document.createElement('div');

return render(
<SnapshotButton filename="daily-snapshot" target={target} {...props} />,
);
};

const clickSnapshot = () =>
fireEvent.click(screen.getByLabelText('Snapshot'), {
preventDefault: jest.fn(),
});
const card = <div>a designed card</div>;

beforeEach(() => {
jest.clearAllMocks();
mockCapture.mockResolvedValue(blob);
mockCopy.mockResolvedValue(true);
// jsdom has neither, and the hook probes both before it will offer a copy.
Object.assign(URL, {
createObjectURL: () => 'blob:preview',
revokeObjectURL: () => undefined,
});
Object.assign(globalThis, { ClipboardItem: class {} });
Object.assign(navigator, { clipboard: { write: async () => undefined } });
});

it('copies the image and says so', async () => {
mockCopy.mockResolvedValue(true);
renderComponent();
const button = () => screen.getByLabelText('Snapshot');

it('does not rasterize the card until there is intent', () => {
render(<SnapshotButton card={card} filename="daily-share" />);

clickSnapshot();
expect(mockCapture).not.toHaveBeenCalled();
});

it('rasterizes on hover, so the press still owns the gesture', async () => {
render(<SnapshotButton card={card} filename="daily-share" />);

fireEvent.pointerEnter(button());

await waitFor(() => expect(mockCapture).toHaveBeenCalledTimes(1));
});

it('rasterizes on keyboard focus too', async () => {
render(<SnapshotButton card={card} filename="daily-share" />);

fireEvent.focus(button());

await waitFor(() => expect(mockCapture).toHaveBeenCalledTimes(1));
});

it('copies the rendered card and says so', async () => {
render(<SnapshotButton card={card} filename="daily-share" />);

fireEvent.pointerEnter(button());
await waitFor(() => expect(mockCapture).toHaveBeenCalled());
fireEvent.click(button());

await waitFor(() =>
expect(mockDisplayToast).toHaveBeenCalledWith('Image copied', {
Expand All @@ -66,45 +89,45 @@ it('copies the image and says so', async () => {
expect(mockDownload).not.toHaveBeenCalled();
});

it('falls back to a download when the clipboard is unavailable', async () => {
it('downloads when the clipboard cannot take an image', async () => {
mockCopy.mockResolvedValue(false);
renderComponent({ filename: 'daily-profile-tomer' });
render(<SnapshotButton card={card} filename="daily-achievement-1" />);

clickSnapshot();
fireEvent.pointerEnter(button());
await waitFor(() => expect(mockCapture).toHaveBeenCalled());
fireEvent.click(button());

await waitFor(() =>
expect(mockDownload).toHaveBeenCalledWith(blob, 'daily-profile-tomer'),
expect(mockDownload).toHaveBeenCalledWith(blob, 'daily-achievement-1'),
);
expect(mockDisplayToast).toHaveBeenCalledWith('Image saved', {
variant: 'success',
});
});

it('reports a failed capture instead of copying or downloading', async () => {
it('reports a failed rasterization instead of going quiet', async () => {
mockCapture.mockRejectedValue(new Error('target element has no size'));
mockCopy.mockResolvedValue(false);
renderComponent();
render(<SnapshotButton card={card} filename="daily-share" />);

clickSnapshot();
fireEvent.click(button());

await waitFor(() =>
expect(mockDisplayToast).toHaveBeenCalledWith(
'Could not create the snapshot, please try again',
{ variant: 'error' },
),
);
expect(mockCopy).not.toHaveBeenCalled();
expect(mockDownload).not.toHaveBeenCalled();
});

it('hands the blob to onCapture instead of sharing it', async () => {
const onCapture = jest.fn();
mockCopy.mockResolvedValue(true);
renderComponent({ onCapture });
it('refuses to render without a card or a target', () => {
// eslint-disable-next-line no-console
const error = jest.spyOn(console, 'error').mockImplementation(() => {});

clickSnapshot();
expect(() => render(<SnapshotButton filename="daily-share" />)).toThrow(
'SnapshotButton needs either a card or a target',
);

await waitFor(() => expect(onCapture).toHaveBeenCalledWith(blob));
expect(mockCopy).not.toHaveBeenCalled();
expect(mockDownload).not.toHaveBeenCalled();
expect(mockDisplayToast).not.toHaveBeenCalled();
error.mockRestore();
});
Loading
Loading