diff --git a/web/libs/editor/tests/e2e/fragments/AtDateTime.ts b/web/libs/editor/tests/e2e/fragments/AtDateTime.ts index fa6eade63de1..9dc1b9b5618a 100644 --- a/web/libs/editor/tests/e2e/fragments/AtDateTime.ts +++ b/web/libs/editor/tests/e2e/fragments/AtDateTime.ts @@ -32,7 +32,7 @@ module.exports = { { id: this._dateInputId }, ); - I.fillField(`#${this._dateInputId}`, "01020304"); + await I.fillField(`#${this._dateInputId}`, "01020304"); const format = await I.executeScript( ({ id }) => { diff --git a/web/libs/editor/tests/e2e/fragments/Modals.js b/web/libs/editor/tests/e2e/fragments/Modals.js index 7313b4e9e409..524a56627909 100644 --- a/web/libs/editor/tests/e2e/fragments/Modals.js +++ b/web/libs/editor/tests/e2e/fragments/Modals.js @@ -1,11 +1,12 @@ const { I } = inject(); module.exports = { - seeWarning(text) { + async seeWarning(text) { I.seeElement(".ant-modal"); I.see("Warning"); + // Wait for modal content to be fully rendered + await I.wait(0.5); I.see(text); - I.waitTicks(3); I.see("OK"); }, dontSeeWarning(text) { diff --git a/web/libs/editor/tests/e2e/tests/audio/audio-regions.test.js b/web/libs/editor/tests/e2e/tests/audio/audio-regions.test.js index 54583134ca61..46b43b9e0dee 100644 --- a/web/libs/editor/tests/e2e/tests/audio/audio-regions.test.js +++ b/web/libs/editor/tests/e2e/tests/audio/audio-regions.test.js @@ -92,22 +92,26 @@ FFlagMatrix(["fflag_feat_front_lsdv_e_278_contextual_scrolling_short"], (flags) // creating a new region I.pressKey("1"); + I.waitTicks(2); // Wait for label selection to update AtAudioView.dragAudioElement(160, 80); I.pressKey("u"); + I.waitTicks(3); // Wait for deselection to complete AtOutliner.seeRegions(2); AtAudioView.clickAt(170); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion(); AtAudioView.clickAt(170); + I.waitTicks(2); // Wait for deselection AtOutliner.dontSeeSelectedRegion(); AtAudioView.dragAudioElement(170, 40); + I.waitTicks(2); // Wait for region selection after drag AtOutliner.seeSelectedRegion(); AtAudioView.clickAt(220); + I.waitTicks(2); // Wait for deselection AtOutliner.dontSeeSelectedRegion(); - }) - .tag("@flakey") - .retry(3); + }); // Don't need to test this for both scenarios of flags, as it is the same code and is verified in the above test if (!flags.fflag_feat_front_lsdv_e_278_contextual_scrolling_short) { @@ -128,14 +132,20 @@ FFlagMatrix(["fflag_feat_front_lsdv_e_278_contextual_scrolling_short"], (flags) for (let i = 0; i < 10; i++) { // creating a new region I.pressKey("1"); + I.waitTicks(2); // Wait for label selection AtLabels.seeSelectedLabel("Speech"); AtAudioView.dragAudioElement(40 * i + 10, 30); + I.waitTicks(2); // Wait for region creation AtOutliner.dontSeeSelectedRegion(); AtAudioView.clickAt(40 * i + 20); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion(); I.pressKey("2"); + I.waitTicks(2); // Wait for label change I.pressKey("1"); + I.waitTicks(2); // Wait for label change I.pressKey("u"); + I.waitTicks(2); // Wait for deselection } AtOutliner.seeRegions(10); @@ -143,19 +153,20 @@ FFlagMatrix(["fflag_feat_front_lsdv_e_278_contextual_scrolling_short"], (flags) for (let i = 0; i < 10; i++) { // creating a new region AtAudioView.clickAt(40 * i + 20); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion(); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection } AtOutliner.seeRegions(10); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection AtOutliner.dontSeeSelectedRegion(); }, - ) - .tag("@flakey") - .retry(3); + ); FFlagScenario("Can select a region below a hidden region", async ({ I, LabelStudio, AtAudioView, AtOutliner }) => { LabelStudio.setFeatureFlags({ @@ -171,31 +182,36 @@ FFlagMatrix(["fflag_feat_front_lsdv_e_278_contextual_scrolling_short"], (flags) // create a new region I.pressKey("1"); + I.waitTicks(2); // Wait for label selection AtAudioView.dragAudioElement(50, 80); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection AtOutliner.seeRegions(1); // create a new region above the first one I.pressKey("2"); + I.waitTicks(2); // Wait for label selection AtAudioView.dragAudioElement(49, 81); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection AtOutliner.seeRegions(2); // click on the top-most region visible to select it AtAudioView.clickAt(51); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion("Noise"); // hide the region AtOutliner.toggleRegionVisibility("Noise"); + I.waitTicks(2); // Wait for visibility change // click on the region below the hidden one to select it AtAudioView.clickAt(51); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion("Speech"); - }) - .tag("@flakey") - .retry(3); + }); FFlagScenario( "Selecting a region brings it to the front of the stack", @@ -213,33 +229,41 @@ FFlagMatrix(["fflag_feat_front_lsdv_e_278_contextual_scrolling_short"], (flags) // create a new region I.pressKey("1"); + I.waitTicks(2); // Wait for label selection AtAudioView.dragAudioElement(50, 80); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection AtOutliner.seeRegions(1); // create a new region above the first one I.pressKey("2"); + I.waitTicks(2); // Wait for label selection AtAudioView.dragAudioElement(49, 81); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection AtOutliner.seeRegions(2); // click on the top-most region visible to select it AtAudioView.clickAt(51); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion("Noise"); // Select the bottom most region to bring it to the top AtOutliner.clickRegion("Speech"); + I.waitTicks(2); // Wait for region selection and z-index change AtOutliner.seeSelectedRegion("Speech"); // click on the overlapping region will deselect it, which shows that it is now the top in the list AtAudioView.clickAt(51); + I.waitTicks(2); // Wait for deselection AtOutliner.dontSeeSelectedRegion("Speech"); AtOutliner.dontSeeSelectedRegion("Noise"); // click on the overlapping region will select the top item of the list, which will now be the item which was brought to the front by the original interaction. AtAudioView.clickAt(51); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion("Speech"); }, ); @@ -282,25 +306,31 @@ FFlagMatrix(["fflag_feat_front_lsdv_e_278_contextual_scrolling_short"], (flags) // creating a new region I.pressKey("1"); + I.waitTicks(2); // Wait for label selection AtAudioView.dragAudioElement(300, 80); I.pressKey("u"); + I.waitTicks(2); // Wait for deselection // creating a ghost region I.pressKey("1"); + I.waitTicks(2); // Wait for label selection AtAudioView.dragAudioElement(160, 80, false); I.pressKey("1"); I.waitTicks(2); I.pressMouseUp(); - I.waitTicks(2); + I.waitTicks(3); // Increased wait for state cleanup // checking if the created region is selected AtAudioView.clickAt(310); + I.waitTicks(2); // Wait for region selection AtOutliner.seeSelectedRegion(); // trying to select the ghost region, if there is no ghost region, the region will keep selected // as ghost region is not selectable and impossible to change the label, the created region will be deselected if there is a ghost region created. AtAudioView.clickAt(170); + I.waitTicks(2); // Wait for click processing I.pressKey("2"); + I.waitTicks(2); // Wait for label change AtOutliner.seeSelectedRegion(); AtOutliner.seeRegions(2); diff --git a/web/libs/editor/tests/e2e/tests/image.transformer.test.js b/web/libs/editor/tests/e2e/tests/image.transformer.test.js index 9fb9af499cf4..828ceeec4e1a 100644 --- a/web/libs/editor/tests/e2e/tests/image.transformer.test.js +++ b/web/libs/editor/tests/e2e/tests/image.transformer.test.js @@ -1,6 +1,7 @@ const assert = require("assert"); const Asserts = require("../utils/asserts"); const Helpers = require("./helpers"); +const { waitForTransformerState } = require("../utils/async-helpers"); Feature("Image transformer"); @@ -171,18 +172,29 @@ Data(shapesTable).Scenario( // Select the first region AtImageView.clickAt(...getCenter(bbox1)); + I.wait(0.1); // Allow click to register AtOutliner.seeSelectedRegion(); + // Wait for transformer to initialize and render + await waitForTransformerState(I, Shape.hasTransformer, "transformer"); + // Match if transformer exist with expectations in single selected mode isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, Shape.hasTransformer); + // Wait for rotator to render + await waitForTransformerState(I, Shape.hasRotator, "rotator"); + // Match if rotator at transformer exist with expectations in single selected mode isTransformerExist = await AtImageView.isRotaterExist(); assert.strictEqual(isTransformerExist, Shape.hasRotator); // Switch to move tool I.pressKey("v"); + I.wait(0.1); // Allow tool switch to register + + // Wait for move tool transformer to initialize + await waitForTransformerState(I, Shape.hasMoveToolTransformer, "transformer"); // Match if rotator at transformer exist with expectations in single selected mode with move tool chosen isTransformerExist = await AtImageView.isTransformerExist(); @@ -190,6 +202,7 @@ Data(shapesTable).Scenario( // Deselect the previous selected region I.pressKey(["u"]); + I.wait(0.1); // Allow deselection to register // Select 2 regions AtImageView.drawThroughPoints( @@ -200,11 +213,18 @@ Data(shapesTable).Scenario( "steps", 10, ); + I.wait(0.1); // Allow multi-selection to complete + + // Wait for multi-selection transformer to initialize + await waitForTransformerState(I, Shape.hasMultiSelectionTransformer, "transformer"); // Match if transformer exist with expectations in multiple selected mode isTransformerExist = await AtImageView.isTransformerExist(); assert.strictEqual(isTransformerExist, Shape.hasMultiSelectionTransformer); + // Wait for multi-selection rotator to initialize + await waitForTransformerState(I, Shape.hasMultiSelectionRotator, "rotator"); + // Match if rotator exist with expectations in multiple selected mode isTransformerExist = await AtImageView.isRotaterExist(); assert.strictEqual(isTransformerExist, Shape.hasMultiSelectionRotator); @@ -246,14 +266,23 @@ Data(shapesTable.filter(({ shapeName }) => shapes[shapeName].hasMoveToolTransfor // Transform the shape // Move the top anchor up for 50px (limited by image border) => {x1:50,y1:0,x2:150,y2:150} AtImageView.drawByDrag(100, 50, 0, -100); + I.waitTicks(3); // Wait for transformation to complete // Move the left anchor left for 50px (limited by image border) => {x1:0,y1:0,x2:150,y2:150} AtImageView.drawByDrag(50, 75, -300, -100); + I.waitTicks(3); // Wait for transformation to complete // Move the right anchor left for 50px => {x1:0,y1:0,x2:100,y2:150} AtImageView.drawByDrag(150, 75, -50, 0); + I.waitTicks(3); // Wait for transformation to complete // Move the bottom anchor down for 100px => {x1:0,y1:0,x2:100,y2:250} AtImageView.drawByDrag(50, 150, 10, 100); + I.waitTicks(3); // Wait for transformation to complete // Move the right-bottom anchor right for 200px and down for 50px => {x1:0,y1:0,x2:300,y2:300} AtImageView.drawByDrag(100, 250, 200, 50); + I.waitTicks(5); // Wait for final transformation to complete + + // Wait for transformer to finish updating and region state to settle + I.wait(0.5); + // Check resulting sizes const rectangleResult = await LabelStudio.serialize(); const exceptedResult = Shape.byBBox(0, 0, 300, 300).result; diff --git a/web/libs/editor/tests/e2e/tests/outliner.test.js b/web/libs/editor/tests/e2e/tests/outliner.test.js index d1a7cb08449c..6c354050722d 100644 --- a/web/libs/editor/tests/e2e/tests/outliner.test.js +++ b/web/libs/editor/tests/e2e/tests/outliner.test.js @@ -1,5 +1,6 @@ const assert = require("assert"); const { centerOfBbox } = require("./helpers"); +const { waitForMetaSaved } = require("../utils/async-helpers"); Feature("Outliner"); @@ -123,6 +124,7 @@ Scenario("Basic details", async ({ I, LabelStudio, AtOutliner, AtDetails }) => { for (let idx = keys.length - 1; idx >= 0; idx--) { I.pressKeyUp(keys[idx]); } + I.wait(0.05); // Small wait between key sequences } }; @@ -195,14 +197,34 @@ Scenario("Basic details", async ({ I, LabelStudio, AtOutliner, AtDetails }) => { I.say("Add new meta and check result"); AtDetails.clickEditMeta(); - - fillByPressKeyDown([["M"], ["Space"], ["1"], ["Shift", "Enter"], ["M"], ["Space"], ["2"], ["Enter"]]); + + // Use fillMeta helper for reliability + AtDetails.fillMeta("M 1\nM 2"); + + // Click outside the meta field to blur it and trigger save + I.click(locate(".lsf-panel__title").withText("Details")); + I.wait(0.3); // Wait for meta to be saved after blur + AtDetails.seeMeta("M 1"); AtDetails.seeMeta("M 2"); + // Wait for first meta to be fully committed + await waitForMetaSaved(I, 2, "M 1", { timeout: 5000 }); + I.say("Add line to meta"); AtDetails.clickMeta(); - fillByPressKeyDown([["Shift", "Enter"], ["3"], ["Enter"]]); + I.wait(0.3); // Wait for meta input to focus + + // Clear and fill with updated meta value + AtDetails.fillMeta("M 1\nM 2\n3"); + + // Click outside the meta field to blur it and trigger save + I.click(locate(".lsf-panel__title").withText("Details")); + I.wait(0.3); // Wait for meta to be saved after blur + + // Wait for meta to be actually saved in the annotation + await waitForMetaSaved(I, 2, "3", { timeout: 5000 }); + AtDetails.seeMeta("3"); AtDetails.dontSeeMeta("23"); diff --git a/web/libs/editor/tests/e2e/tests/paragraphs-enhanced.test.js b/web/libs/editor/tests/e2e/tests/paragraphs-enhanced.test.js index b9361eee51be..445fea6f4fc0 100644 --- a/web/libs/editor/tests/e2e/tests/paragraphs-enhanced.test.js +++ b/web/libs/editor/tests/e2e/tests/paragraphs-enhanced.test.js @@ -77,7 +77,8 @@ async function tryHotkeys(I, combos) { for (const keys of combos) { I.say(`Trying hotkey: ${JSON.stringify(keys)}`); I.pressKey(keys); - I.wait(1.5); // Increased from 0.5s - hotkeys trigger complex DOM/MobX updates + // Wait for hotkeys to trigger DOM/MobX updates using animation frames + I.waitTicks(10); // ~160ms - enough for complex DOM/MobX updates } } @@ -85,7 +86,8 @@ async function tryHotkeys(I, combos) { async function selectLabelSafely(I, AtLabels, labelText) { I.say(`Selecting label: ${labelText}`); AtLabels.clickLabel(labelText); - I.wait(0.8); // Wait for label selection to fully update UI state + // Wait for label selection to fully update UI state using animation frames + I.waitTicks(5); // ~80ms - enough for UI state updates } Scenario( @@ -119,9 +121,7 @@ Scenario( assert.deepStrictEqual(result[0].value.paragraphlabels, ["General: Positive1"]); }); }, -) - .tag("@flakey") - .retry(3); +); Scenario("Select All button is disabled when no label is selected", async ({ I, LabelStudio, AtOutliner }) => { await retryScenario(async () => { @@ -151,12 +151,12 @@ Scenario("Hotkey for Select All creates region", async ({ I, LabelStudio, AtOutl ["Meta", "Shift", "A"], ["Control", "Shift", "A"], ]); - I.wait(1); + I.waitTicks(8); // Wait for region creation to complete let result = await LabelStudio.serialize(); if (result.length === 0) { // Try Ctrl+Shift+A (Win/Linux) if Cmd+Shift+A didn't work await tryHotkeys(I, [["Control", "Shift", "A"]]); - I.wait(1); + I.waitTicks(8); // Wait for region creation to complete result = await LabelStudio.serialize(); } I.say(`Regions after hotkey: ${JSON.stringify(result)}`); @@ -233,7 +233,7 @@ Scenario("Hotkey: Select All and Annotate creates region", async ({ I, LabelStud ["Meta", "Shift", "A"], ["Control", "Shift", "A"], ]); - I.wait(1); + I.waitTicks(8); // Wait for region creation to complete AtOutliner.seeRegions(1); const result = await LabelStudio.serialize(); assert.strictEqual(result.length, 1); @@ -442,7 +442,7 @@ Scenario( I.say("Test 3: Test phrase clicking and selection without audio"); I.click('div[data-testid="phrase:1"]'); - I.wait(0.5); + I.waitTicks(3); // Wait for visual selection update I.say("Clicked phrase 1 - should update visual selection"); I.say("Test 4: Test Select All functionality without audio"); @@ -452,7 +452,7 @@ Scenario( ["Meta", "Shift", "A"], ["Control", "Shift", "A"], ]); - I.wait(1); + I.waitTicks(8); // Wait for region creation to complete AtOutliner.seeRegions(1); I.say("Select All worked without audio - created 1 region"); @@ -464,7 +464,7 @@ Scenario( ["Meta", "ArrowDown"], ["Control", "ArrowDown"], ]); - I.wait(0.5); + I.waitTicks(3); // Wait for phrase navigation I.say("Next phrase hotkey executed without audio"); // Test Previous Phrase hotkey @@ -472,7 +472,7 @@ Scenario( ["Meta", "ArrowUp"], ["Control", "ArrowUp"], ]); - I.wait(0.5); + I.waitTicks(3); // Wait for phrase navigation I.say("Previous phrase hotkey executed without audio"); I.say("Test 6: Test phrase navigation looping at ends without audio"); @@ -482,7 +482,7 @@ Scenario( ["Meta", "ArrowDown"], ["Control", "ArrowDown"], ]); - I.wait(0.2); + I.waitTicks(2); // Wait for phrase navigation } // Try to go beyond last phrase (should loop to first) @@ -490,7 +490,7 @@ Scenario( ["Meta", "ArrowDown"], ["Control", "ArrowDown"], ]); - I.wait(0.5); + I.waitTicks(3); // Wait for phrase navigation I.say("Phrase navigation looping works without audio"); I.say("All tests passed: No audio component functionality works correctly!"); diff --git a/web/libs/editor/tests/e2e/tests/regression-tests/video-timeline-seek-indicator.test.js b/web/libs/editor/tests/e2e/tests/regression-tests/video-timeline-seek-indicator.test.js index 489783ee53b0..606234bfcb8f 100644 --- a/web/libs/editor/tests/e2e/tests/regression-tests/video-timeline-seek-indicator.test.js +++ b/web/libs/editor/tests/e2e/tests/regression-tests/video-timeline-seek-indicator.test.js @@ -40,6 +40,7 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe I.say("Drag the video position indicator to the right to the middle"); AtVideoView.drag(positionBbox, halfway, 0); + I.waitTicks(3); // Wait for drag animation and state update positionBbox = await AtVideoView.grabPositionBoundingRect(); indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); @@ -62,6 +63,7 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe { I.say("Drag the video position indicator to the end"); AtVideoView.drag(positionBbox, trackBbox.width + trackBbox.x); + I.waitTicks(3); // Wait for drag animation and state update positionBbox = await AtVideoView.grabPositionBoundingRect(); indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); @@ -85,6 +87,7 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe I.say("Drag the video position indicator to the left to the middle"); AtVideoView.drag(positionBbox, halfway); + I.waitTicks(3); // Wait for drag animation and state update positionBbox = await AtVideoView.grabPositionBoundingRect(); indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); @@ -107,6 +110,7 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe { I.say("Drag the video position indicator to the start"); AtVideoView.drag(positionBbox, trackBbox.x, 0); + I.waitTicks(3); // Wait for drag animation and state update positionBbox = await AtVideoView.grabPositionBoundingRect(); indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); @@ -138,6 +142,7 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe const maxStepsForward = 5; await AtVideoView.drag(positionBbox, endOfSeeker, 0); + I.waitTicks(3); // Wait for drag animation and state update indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); I.say("Seeker should not have moved"); @@ -146,6 +151,7 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe for (let i = 0; i < maxStepsForward; i++) { I.say("Click on the seek step forward button"); await AtVideoView.clickSeekStepForward(1); + I.waitTicks(2); // Wait for seek step to complete indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); if (indicatorBbox.x > indicatorPosX) break; @@ -157,11 +163,10 @@ Scenario("Seek view should be in sync with indicator position", async ({ I, Labe I.say("Click on the seek step backward button"); await AtVideoView.clickSeekStepBackward(1); + I.waitTicks(2); // Wait for seek step to complete indicatorBbox = await AtVideoView.grabIndicatorBoundingRect(); I.say("Seeker should now have moved to the left"); assert.ok(indicatorBbox.x < indicatorPosX, "Seeker should have moved to the left from this one step movement"); } -}) - .tag("@flakey") - .retry(3); +}); diff --git a/web/libs/editor/tests/e2e/tests/sync/multiple-audio.test.js b/web/libs/editor/tests/e2e/tests/sync/multiple-audio.test.js index 8a2e46922356..180859285ad4 100644 --- a/web/libs/editor/tests/e2e/tests/sync/multiple-audio.test.js +++ b/web/libs/editor/tests/e2e/tests/sync/multiple-audio.test.js @@ -39,7 +39,8 @@ Scenario("Play/pause of multiple synced audio stay in sync", async ({ I, LabelSt } AtAudioView.clickPlayButton(); - I.wait(1); + // Wait for play state to propagate and audio to start playing + I.waitTicks(5); { const [{ paused: audioPaused1 }, { paused: audioPaused2 }] = await AtAudioView.getCurrentAudio(); @@ -47,9 +48,11 @@ Scenario("Play/pause of multiple synced audio stay in sync", async ({ I, LabelSt assert.equal(audioPaused1, false); } - I.wait(1); + // Let audio play for a bit to ensure sync is maintained + I.waitTicks(10); AtAudioView.clickPauseButton(); - I.wait(1); + // Wait for pause state to propagate fully + I.waitTicks(5); { const [{ currentTime: audioTime1, paused: audioPaused1 }, { currentTime: audioTime2, paused: audioPaused2 }] = await AtAudioView.getCurrentAudio(); @@ -64,9 +67,7 @@ Scenario("Play/pause of multiple synced audio stay in sync", async ({ I, LabelSt assert.notEqual(audioTime1, 0); assert.notEqual(audioTime2, 0); } -}) - .tag("@flakey") - .retry(3); +}); /** * @TODO: Fix `The play() request was interrupted by a call to pause()` diff --git a/web/libs/editor/tests/e2e/tests/taxonomy.test.js b/web/libs/editor/tests/e2e/tests/taxonomy.test.js index 83c8238292ba..00a93f66e28a 100644 --- a/web/libs/editor/tests/e2e/tests/taxonomy.test.js +++ b/web/libs/editor/tests/e2e/tests/taxonomy.test.js @@ -49,6 +49,7 @@ Scenario("Lines overlap", async ({ I, LabelStudio, AtTaxonomy }) => { AtTaxonomy.clickTaxonomy(); AtTaxonomy.toggleGroupWithText("target group"); + I.waitTicks(3); // Wait for taxonomy to expand and render await checkOverlapAndGap("long long long", "not so long"); @@ -73,6 +74,8 @@ Scenario("Lines overlap", async ({ I, LabelStudio, AtTaxonomy }) => { AtTaxonomy.clickTaxonomy(); AtTaxonomy.fillSearch("long"); + I.waitTicks(3); // Wait for search results to render + await checkOverlapAndGap("long long long", "not so long"); I.amOnPage("/"); @@ -96,6 +99,8 @@ Scenario("Lines overlap", async ({ I, LabelStudio, AtTaxonomy }) => { AtTaxonomy.clickTaxonomy(); AtTaxonomy.fillSearch("long"); + I.waitTicks(3); // Wait for search results to render + await checkOverlapAndGap("super long line", "enough long line"); await checkOverlapAndGap("enough long line", "not long line"); }); diff --git a/web/libs/editor/tests/e2e/utils/async-helpers.js b/web/libs/editor/tests/e2e/utils/async-helpers.js new file mode 100644 index 000000000000..faf999cc2de0 --- /dev/null +++ b/web/libs/editor/tests/e2e/utils/async-helpers.js @@ -0,0 +1,92 @@ +/** + * Wait for a Konva transformer or rotator to be in a specific state + * @param {Object} I - CodeceptJS I object + * @param {boolean} expectedState - Expected state (true = should exist, false = should not exist) + * @param {string} checkType - Type to check: "transformer" or "rotator" + * @param {Object} options - Configuration options + * @param {number} options.timeout - Timeout in milliseconds (default: 2000) + * @param {number} options.pollInterval - Polling interval in milliseconds (default: 100) + */ +const waitForTransformerState = async (I, expectedState, checkType = "transformer", options = {}) => { + const timeout = options.timeout || 2000; + const pollInterval = options.pollInterval || 100; + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const exists = await I.executeScript( + (checkType) => { + try { + const stage = window.Konva?.stages?.[0]; + if (!stage) return false; + + const selector = checkType === "transformer" ? "._anchor" : ".rotater"; + const elements = stage.find(selector).filter((shape) => shape.getAttr("visible") !== false); + return !!elements.length; + } catch (error) { + return false; + } + }, + checkType + ); + + if (exists === expectedState) { + return; // Success! + } + + await I.wait(pollInterval / 1000); // Convert ms to seconds for CodeceptJS + } + + // If we get here, we timed out + throw new Error(`Timeout waiting for ${checkType} state to be ${expectedState} (elapsed: ${Date.now() - startTime}ms)`); +}; + +/** + * Wait for meta data to be saved in an annotation region + * @param {Object} I - CodeceptJS I object + * @param {number} regionIndex - Index of the region to check + * @param {string} expectedText - Text that should be present in the meta + * @param {Object} options - Configuration options + * @param {number} options.timeout - Timeout in milliseconds (default: 2000) + * @param {number} options.pollInterval - Polling interval in milliseconds (default: 100) + */ +const waitForMetaSaved = async (I, regionIndex, expectedText, options = {}) => { + const timeout = options.timeout || 2000; + const pollInterval = options.pollInterval || 100; + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const saved = await I.executeScript( + (regionIndex, expectedText) => { + try { + const annotations = window.Htx?.annotationStore?.annotations; + if (!annotations || annotations.length === 0) return false; + + const annotation = annotations[0]; + const regions = annotation?.regions; + if (!regions || regions.length <= regionIndex) return false; + + const region = regions[regionIndex]; + return region?.meta?.text && region.meta.text.some((t) => t.includes(expectedText)); + } catch (error) { + return false; + } + }, + regionIndex, + expectedText + ); + + if (saved) { + return; // Success! + } + + await I.wait(pollInterval / 1000); // Convert ms to seconds for CodeceptJS + } + + // If we get here, we timed out + throw new Error(`Timeout waiting for meta to be saved in region ${regionIndex} (elapsed: ${Date.now() - startTime}ms)`); +}; + +module.exports = { + waitForTransformerState, + waitForMetaSaved, +}; diff --git a/web/libs/editor/tests/integration/e2e/control_tags/classification/taxonomy-mig-per-item.cy.ts b/web/libs/editor/tests/integration/e2e/control_tags/classification/taxonomy-mig-per-item.cy.ts index ea99a4cd509e..8993011a5986 100644 --- a/web/libs/editor/tests/integration/e2e/control_tags/classification/taxonomy-mig-per-item.cy.ts +++ b/web/libs/editor/tests/integration/e2e/control_tags/classification/taxonomy-mig-per-item.cy.ts @@ -55,15 +55,13 @@ describe("Control Tags - MIG perItem - Taxonomy", () => { ImageView.waitForImage(); Taxonomy.open(); - ImageView.waitForImage(); - // TODO: Fix this flakey test - // Taxonomy.findItem("Choice 2").as("choice2"); - // cy.wait(50); - // cy.get("@choice2").click(); - - // LabelStudio.serialize().then((result) => { - // expect(result[0]).to.have.property("item_index", 1); - // }); + // Wait for taxonomy to be fully rendered and interactive + Taxonomy.findItem("Choice 2").should("be.visible").should("not.be.disabled"); + Taxonomy.findItem("Choice 2").click(); + + LabelStudio.serialize().then((result) => { + expect(result[0]).to.have.property("item_index", 1); + }); }); it("should be able to create more that one result", () => { @@ -82,7 +80,8 @@ describe("Control Tags - MIG perItem - Taxonomy", () => { ImageView.paginationNextBtn.click(); ImageView.waitForImage(); Taxonomy.open(); - cy.wait(500); + // Wait for taxonomy to be fully rendered and interactive + Taxonomy.findItem("Choice 3").should("be.visible").should("not.be.disabled"); Taxonomy.findItem("Choice 3").click(); LabelStudio.serialize().then((result) => {