Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

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);
});
});