Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

388 lines
14 KiB
JavaScript

/**
* Tests for deletion modules — confirmation_modal.js and delete_manager.js
*
* Focuses on actual workflow behavior: API calls, error handling,
* success/failure callbacks, preview-driven confirmation messages,
* and the formatBytes utility. Skips trivial "API shape" assertions.
*/
// delete_manager.js's formatBytes delegates to the shared window.formatBytes;
// load it first so DeleteManager.formatBytes works under test.
import '@js/utils/format-bytes.js';
import '@js/deletion/confirmation_modal.js';
describe('DeleteConfirmation tooltip lookup', () => {
it('returns the configured tooltip for a known action', () => {
// We test that lookup works without asserting exact tooltip text
const tooltip = window.DeleteConfirmation.getTooltip('deleteDocument');
expect(tooltip).toBeTypeOf('string');
expect(tooltip.length).toBeGreaterThan(0);
});
it('returns a generic fallback for unknown actions', () => {
expect(window.DeleteConfirmation.getTooltip('unknownAction')).toBe('Delete this item');
});
it('marks all destructive actions as dangerous', () => {
const actions = window.DeleteConfirmation.ACTIONS;
// Behavioral invariant: deletion actions whose name starts with "delete"
// and which permanently destroy data must be flagged dangerous.
expect(actions.deleteDocument.dangerous).toBe(true);
expect(actions.deleteCollection.dangerous).toBe(true);
expect(actions.bulkDeleteDocuments.dangerous).toBe(true);
});
it('marks recoverable actions as not dangerous', () => {
const actions = window.DeleteConfirmation.ACTIONS;
// Removing a PDF blob keeps the text content searchable — recoverable.
expect(actions.deleteBlob.dangerous).toBe(false);
expect(actions.removeFromCollection.dangerous).toBe(false);
});
});
// Now load delete_manager and stub its dependencies before importing
let DM;
beforeAll(async () => {
window.api = {
fetchWithErrorHandling: vi.fn(),
getCsrfToken: () => 'test-token',
};
window.showToast = vi.fn();
// Auto-confirm: bypass the modal and just run the action immediately.
// This lets us test the actual API workflow without the modal UI.
window.DeleteConfirmation.confirmAndRun = vi.fn(async (opts, action) => {
await action();
});
await import('@js/deletion/delete_manager.js');
DM = window.DeleteManager;
});
beforeEach(() => {
window.api.fetchWithErrorHandling.mockReset();
window.showToast.mockReset();
window.DeleteConfirmation.confirmAndRun.mockClear();
});
// Flush pending microtasks. delete_manager's skipConfirm path calls
// confirmOptions.onConfirm() WITHOUT awaiting it, so test callers need
// to wait for the inner async work to settle. This is a small wart in
// the source — flushPromises() is the standard workaround.
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
describe('DeleteManager.formatBytes', () => {
it('formats 0 bytes', () => {
expect(DM.formatBytes(0)).toBe('0 Bytes');
});
it('formats bytes < 1024', () => {
expect(DM.formatBytes(500)).toBe('500 Bytes');
});
it('formats kilobytes with decimal precision', () => {
expect(DM.formatBytes(1024)).toBe('1 KB');
expect(DM.formatBytes(1536)).toBe('1.5 KB');
});
it('formats megabytes', () => {
expect(DM.formatBytes(1048576)).toBe('1 MB');
});
it('formats gigabytes', () => {
expect(DM.formatBytes(1073741824)).toBe('1 GB');
});
it('chooses the right unit for boundary values', () => {
expect(DM.formatBytes(1023)).toContain('Bytes');
expect(DM.formatBytes(1024)).toContain('KB');
expect(DM.formatBytes(1024 * 1024 - 1)).toContain('KB');
expect(DM.formatBytes(1024 * 1024)).toContain('MB');
});
});
describe('DeleteManager.deleteDocument', () => {
it('calls DELETE on the document endpoint', async () => {
// First call: preview lookup (GET). Second call: actual DELETE.
window.api.fetchWithErrorHandling
.mockResolvedValueOnce({ success: true, title: 'My Doc', has_blob: false, has_text: false })
.mockResolvedValueOnce({ success: true });
await DM.deleteDocument('doc-123');
// Verify a DELETE was made to the right endpoint
const deleteCall = window.api.fetchWithErrorHandling.mock.calls.find(
([_url, opts]) => opts && opts.method === 'DELETE'
);
expect(deleteCall).toBeDefined();
expect(deleteCall[0]).toContain('doc-123');
});
it('skips the preview when skipConfirm is true', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: true });
await DM.deleteDocument('doc-456', { skipConfirm: true });
await flushPromises();
// Only one call (the DELETE), no preview GET
expect(window.api.fetchWithErrorHandling).toHaveBeenCalledTimes(1);
expect(window.api.fetchWithErrorHandling.mock.calls[0][1].method).toBe('DELETE');
});
it('calls onSuccess callback with API result on success', async () => {
const onSuccess = vi.fn();
const apiResult = { success: true, deleted_id: 'doc-789' };
window.api.fetchWithErrorHandling.mockResolvedValueOnce(apiResult);
await DM.deleteDocument('doc-789', { skipConfirm: true, onSuccess });
await flushPromises();
expect(onSuccess).toHaveBeenCalledWith(apiResult);
});
it('calls onError callback when API returns success: false', async () => {
const onError = vi.fn();
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: false,
error: 'Document is locked',
});
await DM.deleteDocument('doc-locked', { skipConfirm: true, onError });
await flushPromises();
expect(onError).toHaveBeenCalled();
const err = onError.mock.calls[0][0];
expect(err.message).toContain('Document is locked');
});
it('calls onError when fetch throws', async () => {
const onError = vi.fn();
window.api.fetchWithErrorHandling.mockRejectedValueOnce(new Error('Network down'));
await DM.deleteDocument('doc-net', { skipConfirm: true, onError });
await flushPromises();
expect(onError).toHaveBeenCalled();
});
it('does not call onSuccess when API call fails', async () => {
const onSuccess = vi.fn();
window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: false, error: 'fail' });
await DM.deleteDocument('doc-x', { skipConfirm: true, onSuccess, onError: () => {} });
expect(onSuccess).not.toHaveBeenCalled();
});
});
describe('DeleteManager.deleteDocumentBlob', () => {
it('calls DELETE on the document blob endpoint', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: true, bytes_freed: 2048 });
await DM.deleteDocumentBlob('doc-1', { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
expect(call[0]).toContain('doc-1');
expect(call[0]).toContain('blob');
expect(call[1].method).toBe('DELETE');
});
it('passes bytes_freed to onSuccess', async () => {
const onSuccess = vi.fn();
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
bytes_freed: 5000,
});
await DM.deleteDocumentBlob('doc-1', { skipConfirm: true, onSuccess });
await flushPromises();
expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({ bytes_freed: 5000 }));
});
});
describe('DeleteManager.removeFromCollection', () => {
it('calls DELETE on the collection-document endpoint with both IDs', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
document_deleted: false,
});
await DM.removeFromCollection('doc-1', 'coll-2', { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
expect(call[0]).toContain('doc-1');
expect(call[0]).toContain('coll-2');
expect(call[1].method).toBe('DELETE');
});
it('handles document_deleted: true (last collection removal)', async () => {
const onSuccess = vi.fn();
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
document_deleted: true,
});
await DM.removeFromCollection('doc-1', 'coll-2', { skipConfirm: true, onSuccess });
await flushPromises();
expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({ document_deleted: true }));
});
});
describe('DeleteManager.deleteCollection', () => {
it('calls DELETE on the collection endpoint', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
documents_unlinked: 5,
chunks_deleted: 100,
});
await DM.deleteCollection('coll-99', { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
expect(call[0]).toContain('coll-99');
expect(call[1].method).toBe('DELETE');
});
it('reports failure via onError', async () => {
const onError = vi.fn();
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: false,
error: 'Collection in use',
});
await DM.deleteCollection('coll-used', { skipConfirm: true, onError });
await flushPromises();
expect(onError).toHaveBeenCalled();
});
});
describe('DeleteManager.deleteCollectionIndex', () => {
it('calls DELETE on the index endpoint', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
chunks_deleted: 42,
});
await DM.deleteCollectionIndex('coll-1', { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
expect(call[0]).toContain('coll-1');
expect(call[0]).toContain('index');
expect(call[1].method).toBe('DELETE');
});
});
describe('DeleteManager.bulkDeleteDocuments', () => {
it('sends document IDs in the request body', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
deleted: 3,
total: 3,
total_bytes_freed: 1024,
});
await DM.bulkDeleteDocuments(['a', 'b', 'c'], { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
expect(call[1].method).toBe('DELETE');
const body = JSON.parse(call[1].body);
expect(body.document_ids).toEqual(['a', 'b', 'c']);
});
it('reports partial-success counts via onSuccess', async () => {
const onSuccess = vi.fn();
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
deleted: 2,
total: 3,
total_bytes_freed: 2048,
});
await DM.bulkDeleteDocuments(['a', 'b', 'c'], { skipConfirm: true, onSuccess });
await flushPromises();
expect(onSuccess).toHaveBeenCalledWith(
expect.objectContaining({ deleted: 2, total: 3 })
);
});
});
describe('DeleteManager.bulkDeleteBlobs', () => {
it('sends document IDs in the request body', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
deleted: 2,
total_bytes_freed: 1024,
});
await DM.bulkDeleteBlobs(['x', 'y'], { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
const body = JSON.parse(call[1].body);
expect(body.document_ids).toEqual(['x', 'y']);
});
});
describe('DeleteManager.bulkRemoveFromCollection', () => {
it('sends collection ID in URL and document IDs in body', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({
success: true,
unlinked: 3,
deleted: 1,
});
await DM.bulkRemoveFromCollection(['a', 'b', 'c'], 'coll-7', { skipConfirm: true });
await flushPromises();
const call = window.api.fetchWithErrorHandling.mock.calls[0];
expect(call[0]).toContain('coll-7');
expect(call[1].method).toBe('DELETE');
const body = JSON.parse(call[1].body);
expect(body.document_ids).toEqual(['a', 'b', 'c']);
});
});
describe('DeleteManager confirmation flow', () => {
it('skipConfirm: true bypasses DeleteConfirmation.confirmAndRun', async () => {
window.api.fetchWithErrorHandling.mockResolvedValueOnce({ success: true });
await DM.deleteDocument('doc-1', { skipConfirm: true });
await flushPromises();
expect(window.DeleteConfirmation.confirmAndRun).not.toHaveBeenCalled();
});
it('skipConfirm: false routes through confirmation', async () => {
// The first call is the preview lookup
window.api.fetchWithErrorHandling
.mockResolvedValueOnce({ success: true, title: 'Doc' })
.mockResolvedValueOnce({ success: true });
await DM.deleteDocument('doc-2');
expect(window.DeleteConfirmation.confirmAndRun).toHaveBeenCalled();
});
it('preview failure does not block the deletion (warning only)', async () => {
// Preview throws, but the deletion should still proceed via confirmAndRun
window.api.fetchWithErrorHandling
.mockRejectedValueOnce(new Error('preview failed'))
.mockResolvedValueOnce({ success: true });
await DM.deleteDocument('doc-3');
// confirmAndRun was still called → user still gets the dialog
expect(window.DeleteConfirmation.confirmAndRun).toHaveBeenCalled();
});
});