Skip to content

Commit 33827b8

Browse files
authored
Fix pipeline.cy.ts flakes via launcher pinning and form-array sync (#3389)
- createPipeline and focusPipelineEditor clicked launcher cards without pinning to the active main-area widget. JupyterLab keeps launchers in the DOM until Lumino disposes them, so the click could land on a card under a widget that was already retiring. Apply the active-widget pattern from the prior pythoneditor fix and centralize it in clickActiveLauncherCard. resetJupyterLab and createNewScriptEditor now share the same VISIBLE_LAUNCHER_SELECTOR constant. - Add-then-type sequences in the complex-snapshot test typed into form-array inputs that React had not yet mounted. Add a formArrayAdd command that counts inputs under the parent before the Add click and waits for the count to grow before typing. Replaces six occurrences across the test. - The complex-snapshot test wrote complex.pipeline after navigating into pipelines/, so the first directory listing did not contain it and openFile relied on the next file-browser poll to self-heal. Reorder so writeFile runs before openDirectory. --------- Signed-off-by: Luciano Resende <lresende@apple.com>
1 parent 0fba932 commit 33827b8

3 files changed

Lines changed: 57 additions & 64 deletions

File tree

cypress/support/commands.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ import 'cypress-real-events/support';
2222

2323
import '../utils/snapshots/add-commands';
2424

25+
// JupyterLab keeps every launcher widget in the DOM until Lumino disposes it,
26+
// so a launcher mid-tear-down can still expose its cards. Pinning to the
27+
// active main-area widget avoids clicking a card whose owning widget is
28+
// already hidden.
29+
const VISIBLE_LAUNCHER_SELECTOR =
30+
'.jp-MainAreaWidget.jp-Activity:not(.lm-mod-hidden) .jp-Launcher';
31+
32+
const clickActiveLauncherCard = (
33+
cardTitle: string,
34+
category = 'Elyra'
35+
): void => {
36+
cy.get(VISIBLE_LAUNCHER_SELECTOR, { timeout: 10000 }).should('be.visible');
37+
cy.get(
38+
`.jp-LauncherCard[data-category="${category}"][title="${cardTitle}"]:visible`
39+
).click();
40+
};
41+
2542
Cypress.Commands.add('installRuntimeConfig', ({ type } = {}): void => {
2643
const kfpRuntimeInstallCommand =
2744
'elyra-metadata create runtimes \
@@ -153,19 +170,13 @@ Cypress.Commands.add(
153170
if (name === undefined) {
154171
switch (type) {
155172
case 'kfp':
156-
cy.get(
157-
'.jp-LauncherCard[data-category="Elyra"][title="Kubeflow Pipelines Pipeline Editor"]'
158-
).click();
173+
clickActiveLauncherCard('Kubeflow Pipelines Pipeline Editor');
159174
break;
160175
case 'airflow':
161-
cy.get(
162-
'.jp-LauncherCard[data-category="Elyra"][title="Apache Airflow Pipeline Editor"]'
163-
).click();
176+
clickActiveLauncherCard('Apache Airflow Pipeline Editor');
164177
break;
165178
default:
166-
cy.get(
167-
'.jp-LauncherCard[data-category="Elyra"][title="Generic Pipeline Editor"]'
168-
).click();
179+
clickActiveLauncherCard('Generic Pipeline Editor');
169180
break;
170181
}
171182
} else {
@@ -178,9 +189,7 @@ Cypress.Commands.add(
178189
);
179190

180191
Cypress.Commands.add('focusPipelineEditor', (): void => {
181-
cy.get(
182-
'.jp-LauncherCard[data-category="Elyra"][title="Generic Pipeline Editor"]'
183-
).click();
192+
clickActiveLauncherCard('Generic Pipeline Editor');
184193
cy.get('.common-canvas-drop-div').should('be.visible');
185194
});
186195

@@ -252,9 +261,7 @@ Cypress.Commands.add('resetJupyterLab', (): void => {
252261
'exist'
253262
);
254263
// Wait for the launcher to be the active main-area widget (not just present in DOM).
255-
cy.get('.jp-MainAreaWidget.jp-Activity:not(.lm-mod-hidden) .jp-Launcher', {
256-
timeout: 10000
257-
}).should('be.visible');
264+
cy.get(VISIBLE_LAUNCHER_SELECTOR, { timeout: 10000 }).should('be.visible');
258265
});
259266

260267
Cypress.Commands.add('checkTabMenuOptions', (fileType: string): void => {
@@ -282,23 +289,29 @@ Cypress.Commands.add('closeCurrentEditor', (): void => {
282289
});
283290

284291
Cypress.Commands.add('createNewScriptEditor', (language: string): void => {
285-
// Match only a launcher whose parent main-area widget is currently active;
286-
// a hidden launcher (lm-mod-hidden parent) would fail be.visible and flake.
287-
const visibleLauncherSelector =
288-
'.jp-MainAreaWidget.jp-Activity:not(.lm-mod-hidden) .jp-Launcher';
289-
cy.get('body').then(($body) => {
290-
if ($body.find(visibleLauncherSelector).length === 0) {
291-
// Self-heal: no active launcher (e.g. another widget is the active tab).
292-
cy.findByRole('menuitem', { name: /^file$/i }).click();
293-
cy.findByText(/^new launcher$/i).click({ force: true });
294-
}
295-
});
296-
cy.get(visibleLauncherSelector, { timeout: 10000 }).should('be.visible');
297-
cy.get(
298-
`.jp-LauncherCard[data-category="Elyra"][title="Create a new ${language} Editor"]:visible`
299-
).click();
292+
clickActiveLauncherCard(`Create a new ${language} Editor`);
300293
});
301294

295+
// Click an "Add" button inside a form-array section, then type `value` into
296+
// the newly mounted input. Waits for the input count under `parentSelector`
297+
// to increase, closing the race where typing starts before React has
298+
// rendered the new <input>.
299+
Cypress.Commands.add(
300+
'formArrayAdd',
301+
(parentSelector: string, value: string): void => {
302+
cy.get(parentSelector).then(($parent) => {
303+
const prevCount = $parent.find('input').length;
304+
cy.get(parentSelector).within(() => {
305+
cy.findByRole('button', { name: /add/i }).click();
306+
});
307+
cy.get(parentSelector)
308+
.find('input')
309+
.should('have.length.greaterThan', prevCount);
310+
cy.get(parentSelector).find('input').last().type(value);
311+
});
312+
}
313+
);
314+
302315
Cypress.Commands.add('checkScriptEditorToolbarContent', (): void => {
303316
cy.get('.elyra-ScriptEditor .jp-Toolbar');
304317

cypress/support/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ declare namespace Cypress {
4949
openHelloWorld(fileExtension: string): Chainable<void>;
5050
dismissAssistant(fileType: string): Chainable<void>;
5151
focusPipelineEditor(): Chainable<void>;
52+
formArrayAdd(parentSelector: string, value: string): Chainable<void>;
5253
}
5354
}

cypress/tests/pipeline.cy.ts

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,11 @@ describe('Pipeline Editor tests', () => {
154154
cy.bootstrapFile('scripts/setup.txt');
155155

156156
// Do this all manually because our command doesn't support directories yet
157-
cy.openDirectory('pipelines');
157+
// Write the pipeline file before navigating into the directory so the
158+
// file-browser's first listing inside `pipelines/` already contains it,
159+
// avoiding a poll-cycle wait inside openFile.
158160
cy.writeFile('build/cypress/pipelines/complex.pipeline', emptyPipeline);
161+
cy.openDirectory('pipelines');
159162
cy.openFile('complex.pipeline');
160163
cy.get('.common-canvas-drop-div').should('be.visible');
161164

@@ -186,18 +189,10 @@ describe('Pipeline Editor tests', () => {
186189
cy.openDirectory('producer.ipynb');
187190
});
188191

189-
cy.get('#jp-main-dock-panel').within(() => {
190-
cy.get('#root_component_parameters_outputs').within(() => {
191-
cy.findByRole('button', { name: /add/i }).click();
192-
cy.get('input[id="root_component_parameters_outputs_0"]').type(
193-
'output-1.csv'
194-
);
192+
cy.formArrayAdd('#root_component_parameters_outputs', 'output-1.csv');
193+
cy.formArrayAdd('#root_component_parameters_outputs', 'output-2.csv');
195194

196-
cy.findByRole('button', { name: /add/i }).click();
197-
cy.get('input[id="root_component_parameters_outputs_1"]').type(
198-
'output-2.csv'
199-
);
200-
});
195+
cy.get('#jp-main-dock-panel').within(() => {
201196
cy.get('#root_component_parameters_runtime_image').within(() => {
202197
selectRuntimeImage();
203198
});
@@ -231,35 +226,19 @@ describe('Pipeline Editor tests', () => {
231226
cy.get('#root_component_parameters_runtime_image').within(() => {
232227
selectRuntimeImage();
233228
});
234-
cy.get('#root_component_parameters_outputs').within(() => {
235-
cy.findByRole('button', { name: /add/i }).click();
236-
cy.get('input[id="root_component_parameters_outputs_0"]').type(
237-
'input-1.csv'
238-
);
239-
240-
cy.findByRole('button', { name: /add/i }).click();
241-
cy.get('input[id="root_component_parameters_outputs_1"]').type(
242-
'input-2.csv'
243-
);
244-
});
229+
});
230+
cy.formArrayAdd('#root_component_parameters_outputs', 'input-1.csv');
231+
cy.formArrayAdd('#root_component_parameters_outputs', 'input-2.csv');
245232

233+
cy.get('#jp-main-dock-panel').within(() => {
246234
// producer-script props
247235
cy.findByText('producer-script.py').click();
248236
cy.get('#root_component_parameters_runtime_image').within(() => {
249237
selectRuntimeImage();
250238
});
251-
cy.get('#root_component_parameters_outputs').within(() => {
252-
cy.findByRole('button', { name: /add/i }).click();
253-
cy.get('input[id="root_component_parameters_outputs_0"]').type(
254-
'output-3.csv'
255-
);
256-
257-
cy.findByRole('button', { name: /add/i }).click();
258-
cy.get('input[id="root_component_parameters_outputs_1"]').type(
259-
'output-4.csv'
260-
);
261-
});
262239
});
240+
cy.formArrayAdd('#root_component_parameters_outputs', 'output-3.csv');
241+
cy.formArrayAdd('#root_component_parameters_outputs', 'output-4.csv');
263242

264243
cy.savePipeline();
265244

0 commit comments

Comments
 (0)