This is an automated email from the ASF dual-hosted git repository.
aglinxinyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new c1a8d0f07e test(frontend): re-enable two skipped drag-drop tests
(#4971)
c1a8d0f07e is described below
commit c1a8d0f07ec81901872889db2ba635360e5b356f
Author: Yicong Huang <[email protected]>
AuthorDate: Sat May 9 15:13:47 2026 -0700
test(frontend): re-enable two skipped drag-drop tests (#4971)
### What changes were proposed in this PR?
The two tests in `drag-drop.service.spec.ts` parked under #4866 turn out
not to need browser mode at all — both root causes are jsdom-friendly.
**`should find 3 input/output operatorPredicates`** —
`findClosestOperators` was called *before* `addOperator`, so the
captured result was always `[]`. Reorder, and relax the assertion from a
strict ordered `toEqual([…])` to `arrayContaining` + `toHaveLength`
since the function returns its closest-N priority queue as an unsorted
heap.
**`should update highlighting, add operator, and add links when an
operator is dropped`** — drive the real flow through
`attachMainJointPaper` (real paper on a hidden host),
`dragStarted("MultiInputOutput")`, a synthetic `window.dispatchEvent(new
MouseEvent('mousemove', ...))` to populate the suggestion pipeline, and
`dragDropped`. No mocks. Inputs are placed at `x=-100` and outputs at
`x=100` because jsdom's polyfilled `pageToLocalPoint` always resolves to
(0, 0) — fine for input/output classification which only compares
operator x against mouse x.
### Any related issues, documentation, discussions?
Part of #4866 — re-enables the drag-drop half. The remaining
`workflow-editor.component.spec.ts` is still excluded for an unrelated
reason: it transitively pulls in `download.service.ts`'s top-level
`require("content-disposition")`, which esbuild can't rewrite for the
browser bundle. That cleanup belongs in a separate PR, and #4866's scope
can be revisited then — neither blocker is actually a browser-mode
issue.
### How was this PR tested?
`yarn test`: 271 pass, 9 skip, 2 todo (jsdom; +2 from baseline). `yarn
format:ci` clean.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 4.7 (1M context)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../service/drag-drop/drag-drop.service.spec.ts | 180 +++++++++++----------
1 file changed, 99 insertions(+), 81 deletions(-)
diff --git
a/frontend/src/app/workspace/service/drag-drop/drag-drop.service.spec.ts
b/frontend/src/app/workspace/service/drag-drop/drag-drop.service.spec.ts
index 3109dc6943..121c5109c7 100644
--- a/frontend/src/app/workspace/service/drag-drop/drag-drop.service.spec.ts
+++ b/frontend/src/app/workspace/service/drag-drop/drag-drop.service.spec.ts
@@ -81,11 +81,7 @@ describe("DragDropService", () => {
expect(createdLink.target).toEqual(mockScanResultLink.target);
});
- // findClosestOperators consults real SVG geometry (getBBox / getScreenCTM).
- // The jsdom polyfill returns identity matrices and zero-size boxes, so all
- // operators report position (0,0) and the closest-N query yields []. Tracked
- // for re-enable under Vitest browser mode in #4866.
- it.skip("should find 3 input operatorPredicates and 3 output
operatorPredicates for an operatorPredicate with 3 input / 3 output ports", ()
=> {
+ it("should find 3 input operatorPredicates and 3 output operatorPredicates
for an operatorPredicate with 3 input / 3 output ports", () => {
const workflowActionService: WorkflowActionService =
TestBed.inject(WorkflowActionService);
const workflowUtilService: WorkflowUtilService =
TestBed.inject(WorkflowUtilService);
@@ -95,10 +91,6 @@ describe("DragDropService", () => {
const output1 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
const output2 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
const output3 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
- const [inputOps, outputOps] = (dragDropService as
any).findClosestOperators(
- { x: 50, y: 0 },
- mockMultiInputOutputPredicate
- );
workflowActionService.addOperator(input1, { x: 0, y: 0 });
workflowActionService.addOperator(input2, { x: 0, y: 10 });
@@ -107,8 +99,20 @@ describe("DragDropService", () => {
workflowActionService.addOperator(output2, { x: 100, y: 10 });
workflowActionService.addOperator(output3, { x: 100, y: 20 });
- expect(inputOps).toEqual([input1, input2, input3]);
- expect(outputOps).toEqual([output1, output2, output3]);
+ // Probe at the centroid between the input and output columns. With the
+ // SUGGESTION_DISTANCE_THRESHOLD = 300, all 6 operators are in range; the
+ // 3 to the left are ranked as inputs, the 3 to the right as outputs.
+ // Order within each list is heap-internal and not guaranteed by the
+ // implementation — assert membership only.
+ const [inputOps, outputOps] = (dragDropService as
any).findClosestOperators(
+ { x: 50, y: 0 },
+ mockMultiInputOutputPredicate
+ );
+
+ expect(inputOps).toHaveLength(3);
+ expect(inputOps).toEqual(expect.arrayContaining([input1, input2, input3]));
+ expect(outputOps).toHaveLength(3);
+ expect(outputOps).toEqual(expect.arrayContaining([output1, output2,
output3]));
});
it('should publish operatorPredicates to highlight streams when calling
"updateHighlighting(prevHighlights,newHighlights)"', async () => {
@@ -155,81 +159,95 @@ describe("DragDropService", () => {
expect(inputOps).toEqual([]);
});
- // Same root cause as the skipped test above — link inference depends on
- // findClosestOperators returning real geometry. Tracked in #4866.
- it.skip(
- "should update highlighting, add operator, and add links when an operator
is dropped",
- marbles(async () => {
- const workflowActionService: WorkflowActionService =
TestBed.inject(WorkflowActionService);
- const workflowUtilService: WorkflowUtilService =
TestBed.inject(WorkflowUtilService);
- workflowActionService.getJointGraphWrapper();
- const operatorType = "MultiInputOutput";
- const operator = mockMultiInputOutputPredicate;
- const input1 = workflowUtilService.getNewOperatorPredicate("ScanSource");
- const input2 = workflowUtilService.getNewOperatorPredicate("ScanSource");
- const input3 = workflowUtilService.getNewOperatorPredicate("ScanSource");
- const output1 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
- const output2 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
- const output3 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
- const heightSortedInputs: OperatorPredicate[] = [input1, input2, input3];
- const heightSortedOutputs: OperatorPredicate[] = [output1, output2,
output3];
-
- // lists to be populated by observables/streams
- const highlights: string[] = [];
- const unhighlights: string[] = [];
- const links: OperatorLink[] = [];
- // expected end results of above lists
- const expectedHighlights: OperatorPredicate[] = []; // expected empty
- const expectedUnhighlights = [
- input1.operatorID,
- input2.operatorID,
- input3.operatorID,
- output1.operatorID,
- output2.operatorID,
- output3.operatorID,
- ];
- const expectedLinks: OperatorLink[] = []; // NOT EXPECTED EMPTY:
populated below
-
- // populate expected links.
- heightSortedInputs.forEach(inputOperator => {
- expectedLinks.push((dragDropService as
any).getNewOperatorLink(inputOperator, operator, expectedLinks));
- });
- heightSortedOutputs.forEach(outputOperator => {
- expectedLinks.push((dragDropService as
any).getNewOperatorLink(operator, outputOperator, expectedLinks));
- });
-
- const timeout = new Promise(resolve => setTimeout(resolve, 500)); //
await 500ms before checking expect(s), since observables are async
-
- // add operators to graph
- workflowActionService.addOperator(input1, { x: 0, y: 10 });
- workflowActionService.addOperator(input2, { x: 0, y: 20 });
- workflowActionService.addOperator(input3, { x: 0, y: 30 });
+ it("should add the dropped operator with links to suggested neighbors and
unhighlight prior suggestions", async () => {
+ const workflowActionService: WorkflowActionService =
TestBed.inject(WorkflowActionService);
+ const workflowUtilService: WorkflowUtilService =
TestBed.inject(WorkflowUtilService);
+ const input1 = workflowUtilService.getNewOperatorPredicate("ScanSource");
+ const input2 = workflowUtilService.getNewOperatorPredicate("ScanSource");
+ const input3 = workflowUtilService.getNewOperatorPredicate("ScanSource");
+ const output1 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
+ const output2 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
+ const output3 =
workflowUtilService.getNewOperatorPredicate(VIEW_RESULT_OP_TYPE);
+
+ // Real main jointjs paper attached to a hidden DOM host so coordinate
+ // transforms in `dragStarted` / mousemove / `dragDropped` resolve
+ // without stubs. jsdom doesn't compute layout, so the SVG polyfill's
+ // identity matrices collapse `pageToLocalPoint(x, y)` to (0, 0)
+ // regardless of input — that's why operators are placed at x=±100
+ // around the origin below.
+ const paperHost = document.createElement("div");
+ const flyingOpHost = document.createElement("div");
+ flyingOpHost.id = "flyingOP";
+ document.body.appendChild(paperHost);
+ document.body.appendChild(flyingOpHost);
+ try {
+ workflowActionService.getJointGraphWrapper().attachMainJointPaper({ el:
paperHost });
+
+ // Inputs at negative x and outputs at positive x so the (0, 0) drop
+ // point classifies them correctly via `findClosestOperators` (which
+ // compares operator x against mouse x).
+ workflowActionService.addOperator(input1, { x: -100, y: 10 });
+ workflowActionService.addOperator(input2, { x: -100, y: 20 });
+ workflowActionService.addOperator(input3, { x: -100, y: 30 });
workflowActionService.addOperator(output1, { x: 100, y: 10 });
workflowActionService.addOperator(output2, { x: 100, y: 20 });
workflowActionService.addOperator(output3, { x: 100, y: 30 });
- // subscribe to streams and push them to lists (in order to populate
highlights,unhighlights,links)
-
dragDropService.getOperatorSuggestionHighlightStream().subscribe(operatorID => {
- highlights.push(operatorID);
- });
-
dragDropService.getOperatorSuggestionUnhighlightStream().subscribe(operatorID
=> {
- unhighlights.push(operatorID);
- });
+ const unhighlights: string[] = [];
+ dragDropService.getOperatorSuggestionUnhighlightStream().subscribe(id =>
unhighlights.push(id));
+ const links: OperatorLink[] = [];
workflowActionService
.getTexeraGraph()
.getLinkAddStream()
- .subscribe(link => {
- links.push(link);
- });
-
- dragDropService.dragStarted(operatorType);
- dragDropService.dragDropped({ x: 1005, y: 1001 });
-
- // use 500 ms promise to wait for async events to finish executing
- await timeout;
- expect(highlights).toEqual(expectedHighlights as any);
- expect(unhighlights).toEqual(expectedUnhighlights as any);
- expect(links).toEqual(expectedLinks); // depends on custom jasmine
equality comparison function, defined at top in beforeEach{...}
- })
- );
+ .subscribe(link => links.push(link));
+
+ // dragStarted creates a fresh `op` of the given type and subscribes
+ // to window mousemove to populate suggestionInputs / suggestionOutputs.
+ dragDropService.dragStarted("MultiInputOutput");
+ const droppedOp = (dragDropService as any).op as OperatorPredicate;
+
+ // Drive the suggestion pipeline. Any mousemove will do — jsdom's
+ // `pageToLocalPoint` collapses to (0, 0) regardless of the
+ // dispatched coordinates.
+ window.dispatchEvent(new MouseEvent("mousemove", { clientX: 0, clientY:
0 }));
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ dragDropService.dragDropped({ x: 0, y: 0 });
+ // Tear down the window-level mousemove subscriptions installed by
+ // `dragStarted`. Without this the `first()` mouseup observer stays
+ // armed and a stray mousemove from a later spec re-enters this
+ // service's suggestion pipeline.
+ window.dispatchEvent(new MouseEvent("mouseup"));
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ // Each suggested operator should have been unhighlighted at drop time.
+ expect(unhighlights).toEqual(
+ expect.arrayContaining([
+ input1.operatorID,
+ input2.operatorID,
+ input3.operatorID,
+ output1.operatorID,
+ output2.operatorID,
+ output3.operatorID,
+ ])
+ );
+ expect(unhighlights).toHaveLength(6);
+
+ // 3 input→droppedOp links and 3 droppedOp→output links.
+ expect(links).toHaveLength(6);
+ const inputLinks = links.filter(l => l.target.operatorID ===
droppedOp.operatorID);
+ const outputLinks = links.filter(l => l.source.operatorID ===
droppedOp.operatorID);
+ expect(inputLinks.map(l => l.source.operatorID).sort()).toEqual(
+ [input1.operatorID, input2.operatorID, input3.operatorID].sort()
+ );
+ expect(outputLinks.map(l => l.target.operatorID).sort()).toEqual(
+ [output1.operatorID, output2.operatorID, output3.operatorID].sort()
+ );
+ } finally {
+ // Always clean up the DOM hosts even if an assertion above threw,
+ // so the JointJS papers don't leak into later specs.
+ document.body.removeChild(paperHost);
+ document.body.removeChild(flyingOpHost);
+ }
+ });
});