const assert = require("assert");
const { omitBy } = require("./helpers");
Feature("Paragraphs Enhanced - Select All and Hotkeys");
/*
* TIMING STRATEGY FOR FLAKY TESTS:
* - 1.5s after hotkeys (complex DOM/MobX updates)
* - 1.5s after Escape key (MobX state clearing)
* - 2s after region creation attempts (DOM updates and span creation)
* - 0.8s after label selection (UI state updates)
* - 8-10s for waitForElement timeouts (focus detection, complex selectors)
*/
const AUDIO = "/public/files/barradeen-emotional.mp3";
const DATA = {
audio: AUDIO,
dialogue: [
{ author: "Speaker A", text: "This is the first phrase for testing", start: 0, end: 2 },
{ author: "Speaker B", text: "This is the second phrase with different content", start: 2, end: 4 },
{ author: "Speaker A", text: "This is the third phrase for more testing", start: 4, end: 6 },
],
};
const CONFIG = `
`;
const FEATURE_FLAGS = {
ff_front_dev_2669_paragraph_author_filter_210622_short: true,
fflag_fix_front_dev_2918_labeling_filtered_paragraphs_250822_short: true,
fflag_feat_front_bros_199_enable_select_all_in_ner_phrase_short: true,
fflag_feat_front_lsdv_e_278_new_paragraphs_ui_short: true,
};
async function retryScenario(fn, retries = 3) {
let lastErr;
for (let i = 0; i < retries; i++) {
try {
await fn();
return;
} catch (err) {
lastErr = err;
if (i < retries - 1) {
// eslint-disable-next-line no-console
console.warn(`Retrying scenario due to error: ${err}`);
}
}
}
throw lastErr;
}
// Utility to try both Mac and Win/Linux hotkey combos
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
}
}
// Helper to safely select a label with proper wait time
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
}
Scenario(
"Select All button appears and works when label is selected",
async ({ I, LabelStudio, AtOutliner, AtParagraphs, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtOutliner.seeRegions(0);
// Debug: Wait for any label to appear and print all label texts
I.waitForElement(".lsf-label", 10);
const labelTexts = await I.grabTextFromAll(".lsf-label");
I.say(`Visible labels: ${JSON.stringify(labelTexts)}`);
// Wait for the specific label to appear
I.waitForElement(locate(".lsf-label").withText("General: Positive1"), 10); // Increased timeout
AtLabels.clickLabel("General: Positive1");
// Wait for the Select All button to be enabled in the phrase:0 div
I.waitForElement('div[data-testid="phrase:0"] button:not([disabled])', 10);
I.say("Select All button is now enabled and visible");
// Click the Select All button
I.click('div[data-testid="phrase:0"] button');
AtOutliner.seeRegions(1);
const result = await LabelStudio.serialize();
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].value.text, "This is the first phrase for testing");
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 () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtOutliner.seeRegions(0);
// Wait for the Select All button to be present and disabled
I.waitForElement('div[data-testid="phrase:0"] button[disabled]', 10);
I.say("Select All button is present and disabled when no label is selected");
});
});
Scenario("Hotkey for Select All creates region", async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtOutliner.seeRegions(0);
AtLabels.clickLabel("General: Positive1");
// Focus the phrase
I.click('div[data-testid="phrase:0"]');
// Try Cmd+Shift+A (Mac)
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
I.wait(1);
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);
result = await LabelStudio.serialize();
}
I.say(`Regions after hotkey: ${JSON.stringify(result)}`);
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].value.text, "This is the first phrase for testing");
assert.deepStrictEqual(result[0].value.paragraphlabels, ["General: Positive1"]);
});
});
Scenario(
"Feature flag off: Select All button does not appear",
async ({ I, LabelStudio, AtOutliner, AtParagraphs, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags({
fflag_feat_front_bros_199_enable_select_all_in_ner_phrase_short: false,
fflag_feat_front_lsdv_e_278_new_paragraphs_ui_short: false,
});
LabelStudio.init(params);
AtOutliner.seeRegions(0);
AtLabels.clickLabel("General: Positive1");
AtParagraphs.dontSeeSelectAllButton(0);
});
},
);
Scenario("Hotkey: Next Phrase moves focus to next phrase", async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtLabels.clickLabel("General: Positive1");
// Focus the first phrase
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Meta", "ArrowDown"],
["Control", "ArrowDown"],
]);
// Assert focus moved to phrase:1 (implementation may vary)
I.waitForElement('div[data-testid="phrase:1"].focused, div[data-testid="phrase:1"]:focus', 8); // Increased for focus detection
});
});
Scenario("Hotkey: Previous Phrase moves focus to previous phrase", async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtLabels.clickLabel("General: Positive1");
// Focus the second phrase
I.click('div[data-testid="phrase:1"]');
await tryHotkeys(I, [
["Meta", "ArrowUp"],
["Control", "ArrowUp"],
]);
// Assert focus moved to phrase:0
I.waitForElement('div[data-testid="phrase:0"].focused, div[data-testid="phrase:0"]:focus', 8); // Increased for focus detection
});
});
Scenario("Hotkey: Select All and Annotate creates region", async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtOutliner.seeRegions(0);
AtLabels.clickLabel("General: Positive1");
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
I.wait(1);
AtOutliner.seeRegions(1);
const result = await LabelStudio.serialize();
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].value.text, "This is the first phrase for testing");
assert.deepStrictEqual(result[0].value.paragraphlabels, ["General: Positive1"]);
});
});
Scenario("Hotkey: Next Region in Phrase navigates to next region", async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtLabels.clickLabel("General: Positive1");
// Create two regions
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtLabels.clickLabel("General: Positive1");
I.click('div[data-testid="phrase:1"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtOutliner.seeRegions(2);
// Focus the first region (implementation may vary)
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Control", "ArrowRight"],
["Control", "ArrowRight"],
]);
// Assert region selection moved (implementation may vary)
// You may need to check for a selected class or region highlight
});
});
Scenario(
"Hotkey: Previous Region in Phrase navigates to previous region",
async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtLabels.clickLabel("General: Positive1");
// Create two regions
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtLabels.clickLabel("General: Positive1");
I.click('div[data-testid="phrase:1"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtOutliner.seeRegions(2);
// Focus the second region (implementation may vary)
I.click('div[data-testid="phrase:1"]');
await tryHotkeys(I, [
["Control", "ArrowLeft"],
["Control", "ArrowLeft"],
]);
// Assert region selection moved (implementation may vary)
// You may need to check for a selected class or region highlight
});
},
);
Scenario("Hotkey: Next/Previous Region loops at ends", async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA, config: CONFIG };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtLabels.clickLabel("General: Positive1");
// Create three regions on different phrases (following the pattern of working tests)
I.say("Creating region 1 on phrase 0");
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtLabels.clickLabel("General: Positive1");
I.say("Creating region 2 on phrase 1");
I.click('div[data-testid="phrase:1"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtLabels.clickLabel("General: Positive1");
I.say("Creating region 3 on phrase 2");
I.click('div[data-testid="phrase:2"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
AtOutliner.seeRegions(3);
// Focus the last region (simulate by clicking the last outliner entry)
I.click(locate(".lsf-outliner-item").at(3));
I.say("Focused last region");
// Press Next Region hotkey (should loop to first)
await tryHotkeys(I, [
["Control", "ArrowRight"],
["Control", "ArrowRight"],
]);
// Log which region is selected (by .selected class in outliner)
const selectedIdxAfterNext = await I.executeScript(() => {
const items = Array.from(document.querySelectorAll(".lsf-outliner-item"));
return items.findIndex((item) => item.classList.contains("selected"));
});
I.say(`Selected region after Next Region hotkey: ${selectedIdxAfterNext}`);
// Focus the first region
I.click(locate(".lsf-outliner-item").at(1));
I.say("Focused first region");
// Press Previous Region hotkey (should loop to last)
await tryHotkeys(I, [
["Control", "ArrowLeft"],
["Control", "ArrowLeft"],
]);
const selectedIdxAfterPrev = await I.executeScript(() => {
const items = Array.from(document.querySelectorAll(".lsf-outliner-item"));
return items.findIndex((item) => item.classList.contains("selected"));
});
I.say(`Selected region after Previous Region hotkey: ${selectedIdxAfterPrev}`);
});
});
// Test data without audio component
const DATA_NO_AUDIO = {
dialogue: [
{
author: "Mia Wallace",
text: "Dont you hate that?",
},
{
author: "Vincent Vega:",
text: "Hate what?",
},
{
author: "Mia Wallace:",
text: "Uncomfortable silences. Why do we feel its necessary to yak about nonsense in order to be comfortable?",
},
{
author: "Vincent Vega:",
text: "I dont know. Thats a good question.",
},
{
author: "Mia Wallace:",
text: "Thats when you know you found somebody really special. When you can just shut the door closed a minute, and comfortably share silence.",
},
],
};
const CONFIG_NO_AUDIO = `
`;
Scenario(
"No Audio Component: UI works without audio (phrase selection, hotkeys, select all)",
async ({ I, LabelStudio, AtOutliner, AtLabels }) => {
await retryScenario(async () => {
const params = { data: DATA_NO_AUDIO, config: CONFIG_NO_AUDIO };
I.amOnPage("/");
LabelStudio.setFeatureFlags(FEATURE_FLAGS);
LabelStudio.init(params);
AtOutliner.seeRegions(0);
I.say("Test 1: Verify first phrase is selected by default without audio");
I.waitForElement('div[data-testid="phrase:0"]', 5);
// Check if phrase 0 has active/selected styling (our fix auto-selects first phrase)
I.seeElement('div[data-testid="phrase:0"]');
I.say("Test 2: Verify play buttons are visible but disabled without audio");
I.waitForElement('div[data-testid="phrase:0"] button[disabled]', 5);
I.say("Play button is present and disabled without audio");
I.say("Test 3: Test phrase clicking and selection without audio");
I.click('div[data-testid="phrase:1"]');
I.wait(0.5);
I.say("Clicked phrase 1 - should update visual selection");
I.say("Test 4: Test Select All functionality without audio");
AtLabels.clickLabel("General: Positive1");
I.click('div[data-testid="phrase:0"]');
await tryHotkeys(I, [
["Meta", "Shift", "A"],
["Control", "Shift", "A"],
]);
I.wait(1);
AtOutliner.seeRegions(1);
I.say("Select All worked without audio - created 1 region");
I.say("Test 5: Test hotkey phrase navigation without audio");
I.click('div[data-testid="phrase:0"]');
// Test Next Phrase hotkey
await tryHotkeys(I, [
["Meta", "ArrowDown"],
["Control", "ArrowDown"],
]);
I.wait(0.5);
I.say("Next phrase hotkey executed without audio");
// Test Previous Phrase hotkey
await tryHotkeys(I, [
["Meta", "ArrowUp"],
["Control", "ArrowUp"],
]);
I.wait(0.5);
I.say("Previous phrase hotkey executed without audio");
I.say("Test 6: Test phrase navigation looping at ends without audio");
// Go to last phrase
for (let i = 0; i < 5; i++) {
await tryHotkeys(I, [
["Meta", "ArrowDown"],
["Control", "ArrowDown"],
]);
I.wait(0.2);
}
// Try to go beyond last phrase (should loop to first)
await tryHotkeys(I, [
["Meta", "ArrowDown"],
["Control", "ArrowDown"],
]);
I.wait(0.5);
I.say("Phrase navigation looping works without audio");
I.say("All tests passed: No audio component functionality works correctly!");
});
},
);