wrolpi/app/cypress/support/commands.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

83 lines
4 KiB
JavaScript

import {mount} from 'cypress/react'
// Cypress 14 dropped the separate cypress/react18 entry point; cypress/react mounts React 18 itself.
Cypress.Commands.add('mount', mount);
/*
* Waiting on WROLPi background work.
*
* Most mutating file endpoints only QUEUE a job for the background file worker (move, refresh),
* and /api/files/worker_status reads 'idle' while jobs are still waiting in its queue, so
* neither the response nor the status endpoint says anything about completion. The reliable
* signal is the completion event on /api/events/feed. The pattern: capture the server's clock
* BEFORE starting the work, then poll the feed for a matching event newer than that timestamp.
*
* Events carry no job id, so the match is by name/message and cannot prove WHICH job finished.
* A test that leaves a job queued (e.g. rename queues a parent-directory refresh) must drain
* it before ending, or the next test's wait may be satisfied by the straggler.
*/
// The API decides what time it is (the frontend does the same, in case of clock drift).
Cypress.Commands.add('serverNow', () =>
cy.request('/api/events/feed').its('body.now'));
/*
* cy.waitForEvent({since, event, message?, messagePrefix?, messageContains?, failureEvent?, timeout?})
*
* Poll the events feed until an event newer than `since` matches. `event` is a name or a
* list of names; narrow further with an exact `message`, a `messagePrefix`, or a
* `messageContains` substring. If a `failureEvent` (name or list) shows up first, fail
* immediately with its message instead of timing out.
*
* cy.serverNow().then((since) => {
* // ...start the background work...
* cy.waitForEvent({since, event: 'file_move_completed', failureEvent: 'file_move_failed'});
* });
*/
Cypress.Commands.add('waitForEvent', ({since, event, message, messagePrefix, messageContains, failureEvent, timeout = 60000}) => {
const wanted = [].concat(event);
const failures = failureEvent ? [].concat(failureEvent) : [];
const matches = (e) => {
if (!wanted.includes(e.event)) return false;
if (message) return e.message === message;
if (messagePrefix) return (e.message || '').startsWith(messagePrefix);
if (messageContains) return (e.message || '').includes(messageContains);
return true;
};
const deadline = Date.now() + timeout;
const poll = () => {
cy.request(`/api/events/feed?after=${encodeURIComponent(since)}`).then(({body}) => {
const events = body.events || [];
const failed = events.find((e) => failures.includes(e.event));
expect(failed, failed && `${failed.event}: ${failed.message}`).to.be.undefined;
if (events.some(matches)) return;
expect(Date.now(), `event '${wanted}' not seen within ${timeout}ms -- `
+ 'the file worker may be wedged (restart the api service)').to.be.lessThan(deadline);
cy.wait(1000);
poll();
});
};
poll();
});
/*
* cy.refreshFiles(paths, options) -- refresh files on the live stack and wait for completion.
*
* cy.refreshFiles(['cypress-fb']); // refresh one directory
* cy.refreshFiles([], {timeout: 300000}); // global refresh, longer timeout
*/
Cypress.Commands.add('refreshFiles', (paths = [], {timeout = 60000} = {}) => {
// Refresh-completed event names, by scope (wrolpi/files/worker.py).
const completionEvents = ['directory_refresh', 'files_refreshed', 'global_after_refresh_completed'];
// A single directory announces itself by name; match exactly so another test's refresh
// finishing at the same moment cannot satisfy this wait.
const singleDirectory = paths.length === 1 && !/\.[^/]+$/.test(paths[0]);
const message = singleDirectory ? `Refreshed: ${paths[0].replace(/\/$/, '')}` : undefined;
cy.serverNow().then((since) => {
const body = paths.length ? {paths} : undefined;
cy.request('POST', '/api/files/refresh', body);
cy.waitForEvent({since, event: completionEvents, message, timeout});
});
});