mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com> Co-authored-by: Neha P <neha@openobserve.ai>
497 lines
21 KiB
JavaScript
497 lines
21 KiB
JavaScript
// Workflow folders page object — folders for Workflows + folder-scoped RBAC (merged #14421).
|
|
// Plan: .claude/commands/nvpworkflow/workflow-folders-automation.md
|
|
//
|
|
// The folder rail, the add/edit dialog and the move dialog are the SHARED components
|
|
// (FolderList.vue / AddFolder.vue / MoveAcrossFolders.vue), so under Workflows they still
|
|
// carry `dashboard-*` data-tests. Only the list-side and editor-side selectors are
|
|
// workflow-specific. Do not "fix" the dashboard-prefixed names — they are what ships.
|
|
//
|
|
// Two conventions that bite:
|
|
// * OInput forwards the real <input> to a `-field`-suffixed data-test; filling the wrapper
|
|
// is a silent no-op.
|
|
// * Folder tabs are keyed by folderId, NOT display name, so a name -> tab lookup goes
|
|
// through the vuex store (resolveFolderIdByName).
|
|
|
|
const { expect } = require('@playwright/test');
|
|
const { getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js');
|
|
|
|
const LIST_TIMEOUT_MS = 45000; // K10: the workflows list GET is slow
|
|
const DIALOG_TIMEOUT_MS = 10000;
|
|
const EDITOR_TIMEOUT_MS = 30000; // the editor mounts a canvas; slower than a dialog
|
|
|
|
class WorkflowFoldersPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// ---------- folder rail (shared FolderList.vue) ----------
|
|
this.addFolderBtn = page.locator('[data-test="dashboard-new-folder-btn"]').first();
|
|
this.folderSearchInput = page.locator('[data-test="folder-search-field"]');
|
|
this.folderTabsContainer = page.locator('[data-test="dashboards-folder-tabs"]');
|
|
this.moreIconSelector = '[data-test="dashboard-more-icon"]';
|
|
this.editFolderIcon = page.locator('[data-test="dashboard-edit-folder-icon"]');
|
|
this.deleteFolderIcon = page.locator('[data-test="dashboard-delete-folder-icon"]');
|
|
|
|
// ---------- add / edit folder dialog (shared AddFolder.vue) ----------
|
|
this.folderDialog = page.locator('[data-test="dashboard-folder-dialog"]');
|
|
this.folderNameInput = page.locator('[data-test="dashboard-folder-add-name-field"]');
|
|
this.folderDescInput = page.locator('[data-test="dashboard-folder-add-description-field"]');
|
|
this.folderSaveBtn = page.locator(
|
|
'[data-test="dashboard-folder-dialog"] [data-test="o-dialog-primary-btn"]'
|
|
);
|
|
this.folderCancelBtn = page.locator(
|
|
'[data-test="dashboard-folder-dialog"] [data-test="o-dialog-secondary-btn"]'
|
|
);
|
|
this.folderNameError = page.locator('[data-test="dashboard-folder-add-name-error"]');
|
|
|
|
// ---------- delete confirmation ----------
|
|
this.confirmDeleteDialog = page.locator('[data-test="dashboard-confirm-delete-folder-dialog"]');
|
|
this.confirmDeleteBtn = page.locator(
|
|
'[data-test="dashboard-confirm-delete-folder-dialog"] [data-test="o-dialog-primary-btn"]'
|
|
);
|
|
|
|
// ---------- move dialog ----------
|
|
// The consumer's data-test overrides the inner ODialog's via attr inheritance, so the
|
|
// resolved slug on the dialog content is WorkflowsList.vue's, not "move-across-folders-dialog".
|
|
this.moveDialog = page.locator('[data-test="workflow-move-to-another-folder-dialog"]');
|
|
this.moveCurrentFolderInput = page.locator('[data-test="workflows-folder-move-name-field"]');
|
|
this.moveFolderSelectTrigger = page.locator('[data-test="workflows-index-dropdown-stream_type"]');
|
|
this.moveFolderSelectPopover = page.locator(
|
|
'[data-test="workflows-index-dropdown-stream_type-popover"]'
|
|
);
|
|
this.moveNewFolderBtn = page.locator('[data-test="workflows-folder-move-new-add"]');
|
|
this.moveSubmitBtn = page.locator(
|
|
'[data-test="workflow-move-to-another-folder-dialog"] [data-test="o-dialog-primary-btn"]'
|
|
);
|
|
this.moveCancelBtn = page.locator(
|
|
'[data-test="workflow-move-to-another-folder-dialog"] [data-test="o-dialog-secondary-btn"]'
|
|
);
|
|
|
|
// ---------- workflow list ----------
|
|
this.listPage = page.locator('[data-test="workflows-list-page"]');
|
|
this.listSearchInput = page.locator('[data-test="workflow-list-search-input-field"]');
|
|
this.searchScopeCurrent = page.locator('[data-test="workflow-list-search-scope-current"]');
|
|
this.searchAcrossFoldersToggle = page.locator(
|
|
'[data-test="workflow-list-search-across-folders-toggle"]'
|
|
);
|
|
this.listRefreshBtn = page.locator('[data-test="workflow-list-refresh"]');
|
|
this.listTabs = page.locator('[data-test="workflow-list-tabs"]');
|
|
this.draftTag = page.locator('[data-test="workflow-list-draft-tag"]');
|
|
// Prefix match: folder tabs are keyed by folderId, so a count needs the family, not one id.
|
|
this.anyFolderTab = page.locator('button[data-test^="dashboard-folder-tab-"]');
|
|
|
|
// ---------- editor (reached from a folder's list) ----------
|
|
this.editorPage = page.locator('[data-test="workflow-editor-page"]');
|
|
this.editorNameValue = page.locator('[data-test="workflow-editor-name-value"]');
|
|
|
|
// ---------- editor folder picker (create only) ----------
|
|
this.editorFolderDropdown = page.locator('[data-test="workflow-editor-folder"]');
|
|
this.editorFolderStatic = page.locator('[data-test="workflow-editor-folder-static"]');
|
|
this.inlineFolderTrigger = page.locator('[data-test="inline-select-folder-dropdown"]');
|
|
this.inlineFolderAddBtn = page.locator('[data-test="inline-select-folder-dropdown-add"]');
|
|
this.inlineFolderDialog = page.locator('[data-test="inline-select-folder-dropdown-dialog"]');
|
|
}
|
|
|
|
// ---------- per-name factories ----------
|
|
|
|
moveWorkflowBtn(workflowName) {
|
|
return this.page.locator(`[data-test="workflow-list-${workflowName}-move"]`);
|
|
}
|
|
|
|
workflowRowAnchor(workflowName) {
|
|
// The view control is the only per-row element rendered unconditionally. Pause/start and
|
|
// move are both `v-if="!row.is_draft"` — drafts aren't runnable and can't be moved — so
|
|
// anchoring on either silently breaks every draft assertion.
|
|
return this.page.locator(`[data-test="workflow-list-${workflowName}-view"]`);
|
|
}
|
|
|
|
listTab(value) {
|
|
return this.page.locator(`[data-test="workflow-list-tab-${value}"]`);
|
|
}
|
|
|
|
// ---------- store-backed folder id lookup ----------
|
|
|
|
// Folder tabs carry `data-test="dashboard-folder-tab-<folderId>"`, and folderId is not the
|
|
// display name, so resolve it through the store the app already populates. Keying off store
|
|
// data (never role/text/class) keeps the resulting locator in data-test space.
|
|
async resolveFolderIdByName(folderName) {
|
|
if (folderName === 'default') return 'default';
|
|
return await this.page.evaluate((name) => {
|
|
const root = document.querySelector('#app');
|
|
// @ts-ignore — runtime-only globals exposed by Vue.
|
|
const app = window.__APP__ || (root && root.__vue_app__);
|
|
const store =
|
|
app && app.config && app.config.globalProperties && app.config.globalProperties.$store;
|
|
if (!store) return null;
|
|
const list =
|
|
store.state &&
|
|
store.state.organizationData &&
|
|
store.state.organizationData.foldersByType &&
|
|
store.state.organizationData.foldersByType.workflows;
|
|
if (!Array.isArray(list)) return null;
|
|
const match = list.find((item) => item && item.name === name);
|
|
return match ? match.folderId : null;
|
|
}, folderName);
|
|
}
|
|
|
|
async getTabByName(folderName) {
|
|
const folderId = await this.resolveFolderIdByName(folderName);
|
|
if (!folderId) {
|
|
// The store may not be populated yet. Return a locator that cannot match, so
|
|
// visibility assertions read as "not present" rather than throwing here.
|
|
return this.page.locator(`[data-test="dashboard-folder-tab-__missing__:${folderName}"]`);
|
|
}
|
|
return this.page.locator(`button[data-test="dashboard-folder-tab-${folderId}"]`);
|
|
}
|
|
|
|
// ---------- navigation ----------
|
|
|
|
async goToList(folderId) {
|
|
const org = getOrgIdentifier();
|
|
const folderParam = folderId ? `&folder=${folderId}` : '';
|
|
await this.page.goto(
|
|
`${process.env.ZO_BASE_URL}/web/workflows?org_identifier=${org}${folderParam}`,
|
|
{ timeout: 60000 }
|
|
);
|
|
await this.waitForListReady();
|
|
}
|
|
|
|
async waitForListReady() {
|
|
// K10: the list GET is slow; wait for the page container plus the end of "Loading data".
|
|
await this.listPage.first().waitFor({ state: 'visible', timeout: LIST_TIMEOUT_MS });
|
|
await this.page
|
|
.locator('text=Loading data')
|
|
.waitFor({ state: 'detached', timeout: LIST_TIMEOUT_MS })
|
|
.catch(() => {});
|
|
await this.folderTabsContainer.waitFor({ state: 'visible', timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
// The folder is carried in the URL, so read it back from there rather than from the rail —
|
|
// this is what catches a router.push that replaced the query and dropped `folder`.
|
|
folderIdFromUrl() {
|
|
return new URL(this.page.url()).searchParams.get('folder');
|
|
}
|
|
|
|
async expectFolderInUrl(folderId) {
|
|
await expect
|
|
.poll(() => this.folderIdFromUrl(), { timeout: DIALOG_TIMEOUT_MS })
|
|
.toBe(folderId);
|
|
}
|
|
|
|
// ---------- folder CRUD ----------
|
|
|
|
async clickAddFolder() {
|
|
await this.addFolderBtn.click();
|
|
await expect(this.folderNameInput).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async createFolder(name, description = '') {
|
|
await this.clickAddFolder();
|
|
await this.folderNameInput.fill(name);
|
|
if (description) await this.folderDescInput.fill(description);
|
|
await this.folderSaveBtn.click();
|
|
await this.folderDialog.waitFor({ state: 'hidden', timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async cancelFolderDialog() {
|
|
await this.folderCancelBtn.click();
|
|
await expect(this.folderDialog).toBeHidden({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectFolderSaveBlockedOnEmptyName() {
|
|
await this.folderSaveBtn.click();
|
|
// onSubmit short-circuits on an invalid form, so the dialog must still be open.
|
|
await expect(this.folderDialog).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectFolderTabVisible(folderName) {
|
|
await expect
|
|
.poll(async () => ((await this.resolveFolderIdByName(folderName)) ? 'ready' : 'pending'), {
|
|
timeout: DIALOG_TIMEOUT_MS,
|
|
})
|
|
.toBe('ready');
|
|
const tab = await this.getTabByName(folderName);
|
|
await expect(tab).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectFolderTabNotVisible(folderName) {
|
|
const folderId = await this.resolveFolderIdByName(folderName);
|
|
if (!folderId) return; // unresolvable == definitively absent
|
|
await expect(
|
|
this.page.locator(`button[data-test="dashboard-folder-tab-${folderId}"]`)
|
|
).not.toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectDefaultFolderExists() {
|
|
await expect(await this.getTabByName('default')).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async clickFolderTab(folderName) {
|
|
const tab = await this.getTabByName(folderName);
|
|
await tab.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async clickMoreIcon(folderName) {
|
|
const tab = await this.getTabByName(folderName);
|
|
await tab.hover();
|
|
const moreBtn = tab.locator(this.moreIconSelector);
|
|
await moreBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await moreBtn.click();
|
|
}
|
|
|
|
async expectMoreIconNotVisible(folderName) {
|
|
const tab = await this.getTabByName(folderName);
|
|
await tab.hover();
|
|
await expect(tab.locator(this.moreIconSelector)).not.toBeVisible({ timeout: 3000 });
|
|
}
|
|
|
|
async renameFolder(folderName, newName) {
|
|
await this.clickMoreIcon(folderName);
|
|
await this.editFolderIcon.waitFor({ state: 'visible', timeout: 5000 });
|
|
await this.editFolderIcon.click();
|
|
await expect(this.folderNameInput).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
await this.folderNameInput.fill(newName);
|
|
await this.folderSaveBtn.click();
|
|
await this.folderDialog.waitFor({ state: 'hidden', timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async clickDeleteFolder(folderName) {
|
|
await this.clickMoreIcon(folderName);
|
|
await this.deleteFolderIcon.waitFor({ state: 'visible', timeout: 5000 });
|
|
await this.deleteFolderIcon.click();
|
|
await expect(this.confirmDeleteDialog).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async confirmDeleteFolder() {
|
|
await this.confirmDeleteBtn.click();
|
|
await this.confirmDeleteDialog.waitFor({ state: 'hidden', timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
await this.page.waitForLoadState('networkidle', { timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async searchFolders(query) {
|
|
await this.folderSearchInput.fill('');
|
|
await this.folderSearchInput.fill(query);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async folderTabCount() {
|
|
return await this.anyFolderTab.count();
|
|
}
|
|
|
|
// ---------- move ----------
|
|
|
|
async openMoveDialog(workflowName) {
|
|
// Not hovered into view first: the row's view control owns a large tooltip panel that
|
|
// would then overlay these buttons. The move icon is rendered outright for a published
|
|
// row, so a plain click's actionability wait is enough — no force needed.
|
|
const moveBtn = this.moveWorkflowBtn(workflowName);
|
|
await moveBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await moveBtn.click();
|
|
await expect(this.moveDialog).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async selectMoveDestination(folderName) {
|
|
const folderId = await this.resolveFolderIdByName(folderName);
|
|
if (!folderId) {
|
|
throw new Error(`selectMoveDestination: could not resolve folderId for "${folderName}"`);
|
|
}
|
|
await this.moveFolderSelectTrigger.click();
|
|
await this.moveFolderSelectPopover.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
|
|
const option = this.page.locator(
|
|
`[data-test="workflows-index-dropdown-stream_type-option"][data-test-value="${folderId}"]`
|
|
);
|
|
// This select is NOT searchable, and OSelect virtualizes its options, so a destination
|
|
// below the fold is simply absent from the DOM. Page the list down until it renders
|
|
// rather than waiting on an element that will never appear on its own.
|
|
for (let i = 0; i < 20; i += 1) {
|
|
if (await option.isVisible().catch(() => false)) break;
|
|
await this.moveFolderSelectPopover.press('PageDown').catch(() => {});
|
|
}
|
|
await option.waitFor({ state: 'visible', timeout: DIALOG_TIMEOUT_MS });
|
|
await option.click();
|
|
await this.moveFolderSelectPopover.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async clickMove() {
|
|
await this.moveSubmitBtn.click();
|
|
// Hard assert: the dialog only closes once the PATCH resolves, so swallowing this
|
|
// would let a failed move pass here and break a later test instead.
|
|
await expect(this.moveDialog).toBeHidden({ timeout: 15000 });
|
|
await this.page
|
|
.locator('[data-test="o-dialog-overlay"]')
|
|
.waitFor({ state: 'hidden', timeout: 3000 })
|
|
.catch(() => {});
|
|
await this.page.waitForLoadState('networkidle', { timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async cancelMove() {
|
|
await this.moveCancelBtn.first().click();
|
|
await this.moveDialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
await this.page
|
|
.locator('[data-test="o-dialog-overlay"]')
|
|
.waitFor({ state: 'hidden', timeout: 3000 })
|
|
.catch(() => {});
|
|
}
|
|
|
|
// Submit stays disabled while the destination equals the source — the dialog's
|
|
// only signal that nothing would happen.
|
|
async expectMoveDisabled() {
|
|
await expect(this.moveSubmitBtn).toBeDisabled();
|
|
}
|
|
|
|
async expectMoveEnabled() {
|
|
await expect(this.moveSubmitBtn).toBeEnabled();
|
|
}
|
|
|
|
async moveWorkflowToFolder(workflowName, destinationFolderName) {
|
|
await this.openMoveDialog(workflowName);
|
|
await this.selectMoveDestination(destinationFolderName);
|
|
await this.expectMoveEnabled();
|
|
await this.clickMove();
|
|
}
|
|
|
|
// ---------- list search / scope ----------
|
|
|
|
async searchWorkflows(query) {
|
|
// Clear first — fill()-ing the value the model already holds is a no-op.
|
|
await this.listSearchInput.fill('');
|
|
await this.listSearchInput.fill(query);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async clearWorkflowSearch() {
|
|
await this.listSearchInput.fill('');
|
|
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async setSearchScopeAllFolders() {
|
|
await this.searchAcrossFoldersToggle.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async setSearchScopeCurrentFolder() {
|
|
await this.searchScopeCurrent.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async isSearchAcrossFoldersActive() {
|
|
const state = await this.searchAcrossFoldersToggle.getAttribute('data-state');
|
|
return state === 'on' || state === 'active';
|
|
}
|
|
|
|
async refreshList() {
|
|
await this.listRefreshBtn.click({ timeout: 5000 });
|
|
await this.page.waitForLoadState('networkidle', { timeout: DIALOG_TIMEOUT_MS }).catch(() => {});
|
|
}
|
|
|
|
async selectListTab(value) {
|
|
await this.listTab(value).click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
// Reka's ToggleGroupItem marks the selected tab data-state="on"; pinning it is what
|
|
// proves a following "row is gone" assertion was made under the filter, not mid-render.
|
|
async expectListTabActive(value) {
|
|
await expect(this.listTab(value)).toHaveAttribute('data-state', 'on', {
|
|
timeout: DIALOG_TIMEOUT_MS,
|
|
});
|
|
}
|
|
|
|
// ---------- row assertions ----------
|
|
|
|
async expectWorkflowVisible(workflowName) {
|
|
// Refresh then re-search on each attempt: the list GET is slow and cached, so
|
|
// re-typing alone cannot pull in a row the page has not fetched.
|
|
await expect(async () => {
|
|
if (await this.workflowRowAnchor(workflowName).isVisible().catch(() => false)) return;
|
|
await this.refreshList();
|
|
await this.searchWorkflows(workflowName);
|
|
await expect(this.workflowRowAnchor(workflowName)).toBeVisible({ timeout: 5000 });
|
|
}).toPass({ timeout: LIST_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectWorkflowNotVisible(workflowName) {
|
|
await expect(this.workflowRowAnchor(workflowName)).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectDraftTagOnRow(workflowName) {
|
|
await expect(this.workflowRowAnchor(workflowName)).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
await expect(this.draftTag.first()).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
// Moving a DRAFT between folders is not supported — move_workflows only touches the
|
|
// workflows table, so the list keeps the button published-only.
|
|
async expectMoveUnavailableForDraft(workflowName) {
|
|
await expect(this.moveWorkflowBtn(workflowName)).toHaveCount(0);
|
|
}
|
|
|
|
// ---------- editor, entered from a folder's list ----------
|
|
|
|
// The row's edit control is the way into the editor from a folder listing; owning it here
|
|
// keeps the folder-round-trip specs (WFF-UI-11/12) free of raw selectors.
|
|
editWorkflowBtn(workflowName) {
|
|
return this.page.locator(`[data-test="workflow-list-${workflowName}-edit"]`);
|
|
}
|
|
|
|
async openEditorFromRow(workflowName) {
|
|
await this.editWorkflowBtn(workflowName).click();
|
|
await this.expectEditorVisible();
|
|
}
|
|
|
|
async expectEditorVisible() {
|
|
await expect(this.editorPage).toBeVisible({ timeout: EDITOR_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectEditorNameContains(workflowName) {
|
|
await expect(this.editorNameValue).toContainText(workflowName, { timeout: EDITOR_TIMEOUT_MS });
|
|
}
|
|
|
|
// Re-enter the editor by its own URL — the cold-load path a refresh takes.
|
|
async reloadCurrentUrl() {
|
|
await this.page.goto(this.page.url(), { timeout: 60000 });
|
|
}
|
|
|
|
// ---------- editor folder picker ----------
|
|
|
|
// OSelect VIRTUALIZES its option list — only the visible window is in the DOM — so an
|
|
// option below the fold never becomes visible and the click times out. Orgs accumulate
|
|
// folders, so this fails as a function of how many folders exist, not of the code under
|
|
// test. This select is `searchable`, so filter by name first: that collapses the list to
|
|
// one row regardless of how many folders there are.
|
|
async selectFolderInEditor(folderName) {
|
|
const folderId = await this.resolveFolderIdByName(folderName);
|
|
if (!folderId) {
|
|
throw new Error(`selectFolderInEditor: could not resolve folderId for "${folderName}"`);
|
|
}
|
|
await this.inlineFolderTrigger.click();
|
|
const search = this.page.locator('[data-test="inline-select-folder-dropdown-search"]');
|
|
if (await search.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await search.fill(folderName);
|
|
}
|
|
const option = this.page.locator(
|
|
`[data-test="inline-select-folder-dropdown-option"][data-test-value="${folderId}"]`
|
|
);
|
|
await option.waitFor({ state: 'visible', timeout: DIALOG_TIMEOUT_MS });
|
|
await option.click();
|
|
}
|
|
|
|
// The picker renders only while creating; on an existing workflow the folder is static text.
|
|
async expectEditorFolderPickerVisible() {
|
|
await expect(this.editorFolderDropdown).toBeVisible({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
async expectEditorFolderPickerHidden() {
|
|
await expect(this.editorFolderDropdown).toBeHidden({ timeout: DIALOG_TIMEOUT_MS });
|
|
}
|
|
|
|
// The static text falls back to the folderId when the store has no such folder, so
|
|
// asserting the NAME is what catches an editor that lost the folder it was opened from.
|
|
async expectEditorFolderStatic(folderName) {
|
|
await expect(this.editorFolderStatic).toContainText(folderName, {
|
|
timeout: EDITOR_TIMEOUT_MS,
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = WorkflowFoldersPage;
|