wrolpi/app/cypress/e2e/files/footer-delete.cy.js
lrnselfreliance 9278103554
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
e2e: exercise the FileBrowser footer buttons against real files (#670)
* e2e: exercise the FileBrowser footer buttons against real files

Sixteen Cypress tests covering Rename, Move, Delete (including the
tagged-file force-delete flow), Ignore/Unignore, Upload, and scoped
Refresh, with no API mocking: fb:* tasks seed a cypress-fb/ sandbox
inside the media directory, the UI mutates it through the real API, and
assertions check the disk as well as the table.  Skipped in CI, which
runs only the React dev server.

Background jobs (move, refresh) only queue work and worker_status reads
idle while jobs wait, so cy.refreshFiles / cy.waitForEvent poll the
events feed for completion events newer than the server's clock, and
fail loudly when one never arrives.  Every suite cleans up
unconditionally: sandbox files, DB rows via a final refresh, test tags
and their tags/ directories, and any leaked ignored-directory config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* e2e: act on review — drain stragglers, teardown that cannot be skipped

Events carry no job id, so waitForEvent cannot prove WHICH job finished.
The rename tests now drain the parent-directory refresh their rename
queues before ending, so the next test's refresh wait cannot be
satisfied by a straggler while the DB still reflects the old tree; move
waits match the destination in the completion message.

Teardown must always reach the disk wipe: a failed command aborts the
rest of its hook, so the tag cleanup and the DB-purge refresh are now
best-effort (failOnStatusCode: false, no assertions), and the unignore
runs after every test instead of once per spec.  History and
forward-looking product notes trimmed from comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 20:27:29 -06:00

136 lines
5.2 KiB
JavaScript

/*
* The Delete button in the FileBrowser's sticky footer, exercised against the live stack.
*
* Real files: the fb:* tasks seed a `cypress-fb/` sandbox in the media directory and the UI
* deletes them through the real API (POST /api/files/delete runs synchronously). Deleting a
* TAGGED file is refused with FILE_GROUP_IS_TAGGED, which opens a second confirmation listing
* the tagged files; confirming there force-deletes.
*/
import {
assertWROLModeOff,
dialog,
footerButton,
row,
selectRow,
setupFileBrowser,
teardownFileBrowser,
} from '../../support/file-browser';
// Requires the full stack (API + real media directory); CI runs only the React dev server.
(Cypress.env('CI') ? describe.skip : describe)('FileBrowser footer: Delete', () => {
const seedFiles = {
'fb-notes.txt': 'delete me',
'fb-other.txt': 'leave me alone',
'fb-alpha/fb-one.txt': 'child one',
'fb-alpha/fb-two.txt': 'child two',
'fb-tagged.txt': 'tagged, delete needs force',
};
const TAG_NAME = 'CypressFBDelete';
// Remove the suite's tag wherever a failed run may have left it, along with the
// tags/<TAG_NAME>/ directory: force-deleting a tagged file strands its hardlink there.
// failOnStatusCode: false throughout -- a failed command aborts the rest of its hook,
// and this runs in teardown, where an API hiccup must not skip the disk cleanup.
const deleteTestTag = () => {
cy.request({url: '/api/tag', failOnStatusCode: false}).then(({body}) => {
const tag = ((body && body.tags) || []).find((t) => t.name === TAG_NAME);
if (tag) {
cy.request({
method: 'DELETE',
url: `/api/tag/${tag.id}`,
failOnStatusCode: false,
});
}
});
cy.task('fb:cleanup-tag-dir', TAG_NAME);
};
before(() => {
assertWROLModeOff();
deleteTestTag();
});
beforeEach(() => setupFileBrowser(seedFiles, 'fb-notes.txt'));
after(() => {
deleteTestTag();
teardownFileBrowser();
});
// The footer Delete goes through APIButton's confirmation first.
const confirmDialog = () => {
dialog().should('be.visible')
.should('contain.text', 'Are you sure you want to delete these files?');
return dialog();
};
it('cancelling the confirmation deletes nothing', () => {
selectRow('fb-notes.txt');
footerButton('Delete').click();
confirmDialog().contains('button', 'Cancel').click();
dialog().should('not.exist');
row('fb-notes.txt').should('be.visible');
cy.task('fb:exists', 'fb-notes.txt').should('be.true');
});
it('deletes one file from disk', () => {
selectRow('fb-notes.txt');
footerButton('Delete').click();
confirmDialog().contains('button', 'Delete').click();
cy.contains('tr', 'fb-notes.txt').should('not.exist');
cy.task('fb:exists', 'fb-notes.txt').should('be.false');
cy.task('fb:exists', 'fb-other.txt').should('be.true');
});
it('deletes an open directory recursively without breaking the table', () => {
// Open the folder first so it is in openFolders; deleting it must also prune it
// from openFolders or the next fetch requests a directory that no longer exists.
cy.contains('.file-path', 'fb-alpha').click();
row('fb-one.txt').should('be.visible');
selectRow('fb-alpha');
footerButton('Delete').click();
confirmDialog().contains('button', 'Delete').click();
cy.contains('tr', /fb-alpha$/).should('not.exist');
cy.task('fb:exists', 'fb-alpha').should('be.false');
// The table still renders the remaining files -- no failed re-fetch.
row('fb-other.txt').should('be.visible');
});
it('a tagged file needs the second, force-delete confirmation', () => {
// Tag the file through the real API; the FileGroup exists because setup refreshed.
cy.request('POST', '/api/tag', {name: TAG_NAME, color: '#ff0000'});
cy.request('POST', '/api/files/tag', {
tag_name: TAG_NAME,
file_group_primary_path: 'cypress-fb/fb-tagged.txt',
});
// First attempt: cancel at the tagged-files warning; nothing is deleted.
selectRow('fb-tagged.txt');
footerButton('Delete').click();
confirmDialog().contains('button', 'Delete').click();
dialog().should('contain.text', 'Tagged Files Will Be Deleted')
.should('contain.text', 'cypress-fb/fb-tagged.txt')
.should('contain.text', TAG_NAME);
dialog().contains('button', 'Cancel').click();
dialog().should('not.exist');
cy.task('fb:exists', 'fb-tagged.txt').should('be.true');
// Second attempt: confirm the warning; the file is force-deleted.
footerButton('Delete').click();
confirmDialog().contains('button', 'Delete').click();
dialog().should('contain.text', 'Tagged Files Will Be Deleted');
dialog().contains('button', 'Delete').click();
cy.contains('tr', 'fb-tagged.txt').should('not.exist');
cy.task('fb:exists', 'fb-tagged.txt').should('be.false');
});
});