9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import { normalizeReimbursementInput } from './utils.js';
|
|
import './check-login.js';
|
|
import './reimbursement-draft.js';
|
|
import './reimbursement-plan.js';
|
|
|
|
let tmpDir;
|
|
let receiptPath;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-mercury-test-'));
|
|
receiptPath = path.join(tmpDir, 'receipt.png');
|
|
fs.writeFileSync(receiptPath, 'receipt');
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function validArgs(overrides = {}) {
|
|
return {
|
|
receipt: receiptPath,
|
|
amount: '140.00',
|
|
currency: 'CNY',
|
|
date: '2026-06-26',
|
|
merchant: 'Example Merchant',
|
|
category: 'Marketing & Advertising',
|
|
notes: 'Example business purpose.',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createPageMock(evaluateResults, overrides = {}) {
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
evaluate: vi.fn()
|
|
.mockImplementation(() => {
|
|
if (!evaluateResults.length) throw new Error('unexpected evaluate call');
|
|
return evaluateResults.shift();
|
|
}),
|
|
uploadFiles: vi.fn().mockResolvedValue({
|
|
uploaded: true,
|
|
files: 1,
|
|
file_names: ['receipt.png'],
|
|
target: '[data-testid="expense-attachment-upload"]',
|
|
matches_n: 1,
|
|
match_level: 'exact',
|
|
}),
|
|
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function loggedInState(overrides = {}) {
|
|
return {
|
|
url: 'https://app.mercury.com/expenses/my-expenses',
|
|
loggedIn: true,
|
|
hasSubmitExpense: true,
|
|
hasReimbursements: true,
|
|
title: 'Mercury',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createSurface(overrides = {}) {
|
|
return {
|
|
url: 'https://app.mercury.com/expenses/my-expenses',
|
|
title: 'Mercury',
|
|
hasFinalSubmit: false,
|
|
hasForm: false,
|
|
bodyPreview: 'My Expenses Submit expense',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function clickResult(clicked = true) {
|
|
return { clicked, blocked: false, text: clicked ? 'clicked' : '' };
|
|
}
|
|
|
|
function fillResult(touched = {}) {
|
|
return {
|
|
touched: {
|
|
amount: true,
|
|
currency: true,
|
|
date: true,
|
|
merchant: true,
|
|
category: true,
|
|
notes: true,
|
|
...touched,
|
|
},
|
|
};
|
|
}
|
|
|
|
function reviewState(overrides = {}) {
|
|
return {
|
|
url: 'https://app.mercury.com/expenses/review',
|
|
title: 'Mercury',
|
|
hasReview: true,
|
|
hasSubmitExpenseButton: true,
|
|
bodyPreview: 'Review Submit expense',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('mercury reimbursement input validation', () => {
|
|
it('normalizes valid input and keeps receipt absolute internally', () => {
|
|
const input = normalizeReimbursementInput(validArgs());
|
|
expect(input).toMatchObject({
|
|
receipt: receiptPath,
|
|
amount: '140.00',
|
|
currency: 'CNY',
|
|
date: '2026-06-26',
|
|
merchant: 'Example Merchant',
|
|
category: 'Marketing & Advertising',
|
|
notes: 'Example business purpose.',
|
|
ocrWaitSeconds: 8,
|
|
closeAfterReview: false,
|
|
});
|
|
});
|
|
|
|
it('rejects malformed local-only arguments before browser work', () => {
|
|
expect(() => normalizeReimbursementInput(validArgs({ amount: '0' }))).toThrow(ArgumentError);
|
|
expect(() => normalizeReimbursementInput(validArgs({ amount: '1.999' }))).toThrow(ArgumentError);
|
|
expect(() => normalizeReimbursementInput(validArgs({ currency: 'US' }))).toThrow(ArgumentError);
|
|
expect(() => normalizeReimbursementInput(validArgs({ date: '2026-02-30' }))).toThrow(ArgumentError);
|
|
expect(() => normalizeReimbursementInput(validArgs({ 'ocr-wait-seconds': 'abc' }))).toThrow(ArgumentError);
|
|
expect(() => normalizeReimbursementInput(validArgs({ receipt: path.join(tmpDir, 'missing.png') }))).toThrow(ArgumentError);
|
|
});
|
|
});
|
|
|
|
describe('mercury reimbursement-plan', () => {
|
|
it('validates locally and returns a deterministic basename-only plan', async () => {
|
|
const command = getRegistry().get('mercury/reimbursement-plan');
|
|
const rows = await command.func(validArgs());
|
|
expect(rows).toEqual([expect.objectContaining({
|
|
status: 'ready',
|
|
receipt: 'receipt.png',
|
|
amount: '140.00',
|
|
currency: 'CNY',
|
|
safety: expect.stringContaining('never clicks final Submit expense'),
|
|
})]);
|
|
});
|
|
});
|
|
|
|
describe('mercury reimbursement-draft', () => {
|
|
const command = getRegistry().get('mercury/reimbursement-draft');
|
|
|
|
it('throws AuthRequiredError instead of returning a fake draft when logged out', async () => {
|
|
const page = createPageMock([loggedInState({ loggedIn: false, url: 'https://app.mercury.com/login' })]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(AuthRequiredError);
|
|
expect(page.uploadFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('typed-fails malformed login state payloads', async () => {
|
|
const page = createPageMock([{ url: 'https://app.mercury.com/expenses/my-expenses', title: 'Mercury' }]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
expect(page.uploadFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('refuses to click when an existing review/submit surface is already open', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface({ hasFinalSubmit: true, hasForm: true, bodyPreview: 'Review Submit expense amount merchant' }),
|
|
]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
expect(page.uploadFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('typed-fails if the create click probe sees a submit/review surface', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
{ clicked: false, blocked: true, text: 'Submit expense' },
|
|
]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
expect(page.uploadFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('typed-fails malformed create expense surface payloads', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
{ url: 'https://app.mercury.com/expenses/my-expenses', title: 'Mercury', hasForm: false },
|
|
]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
expect(page.uploadFiles).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('typed-fails upload confirmation drift', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
], {
|
|
uploadFiles: vi.fn().mockResolvedValue({ uploaded: false, files: 0 }),
|
|
});
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('typed-fails upload confirmation for the wrong file name', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
], {
|
|
uploadFiles: vi.fn().mockResolvedValue({ uploaded: true, files: 1, file_names: ['other.png'] }),
|
|
});
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('typed-fails upload confirmation for the wrong input target', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
], {
|
|
uploadFiles: vi.fn().mockResolvedValue({
|
|
uploaded: true,
|
|
files: 1,
|
|
file_names: ['receipt.png'],
|
|
target: '[data-testid="wrong"]',
|
|
matches_n: 1,
|
|
}),
|
|
});
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('typed-fails missed required field selectors before Review', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
fillResult({ merchant: false }),
|
|
]);
|
|
await expect(command.func(page, validArgs())).rejects.toThrow(/merchant/);
|
|
});
|
|
|
|
it('typed-fails if Mercury does not reach Review with final submit visible', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
fillResult(),
|
|
clickResult(true),
|
|
reviewState({ hasReview: false, hasSubmitExpenseButton: false }),
|
|
]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('typed-fails malformed review payloads', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
fillResult(),
|
|
clickResult(true),
|
|
{ url: 'https://app.mercury.com/expenses/review', title: 'Mercury', hasReview: true },
|
|
]);
|
|
await expect(command.func(page, validArgs())).rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('returns review-ready summary without clicking final Submit expense', async () => {
|
|
const page = createPageMock([
|
|
loggedInState(),
|
|
createSurface(),
|
|
clickResult(true),
|
|
fillResult(),
|
|
clickResult(true),
|
|
reviewState(),
|
|
]);
|
|
const rows = await command.func(page, validArgs());
|
|
expect(rows).toEqual([expect.objectContaining({
|
|
status: 'review_ready',
|
|
receipt: 'receipt.png',
|
|
uploaded: true,
|
|
reviewReady: true,
|
|
submitBlocked: true,
|
|
warnings: 'final Submit expense was intentionally not clicked',
|
|
})]);
|
|
expect(page.uploadFiles).toHaveBeenCalledWith('[data-testid="expense-attachment-upload"]', [receiptPath]);
|
|
expect(page.evaluate).toHaveBeenCalledTimes(6);
|
|
});
|
|
});
|