enso/app/dashboard/e2e/actions/DrivePageActions.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

292 lines
11 KiB
TypeScript
Raw Normal View History

/** @file Actions for the "drive" page. */
import * as test from 'playwright/test'
import * as actions from '../actions'
import type * as baseActions from './BaseActions'
import * as contextMenuActions from './contextMenuActions'
import EditorPageActions from './EditorPageActions'
import * as goToPageActions from './goToPageActions'
import NewDataLinkModalActions from './NewDataLinkModalActions'
import PageActions from './PageActions'
import StartModalActions from './StartModalActions'
// =================
// === Constants ===
// =================
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 }
// =======================
// === locateAssetRows ===
// =======================
/** Find all assets table rows (if any). */
function locateAssetRows(page: test.Page) {
return actions.locateAssetsTable(page).locator('tbody').getByRole('row')
}
// ========================
// === DrivePageActions ===
// ========================
/** Actions for the "drive" page. */
export default class DrivePageActions extends PageActions {
/** Actions for navigating to another page. */
get goToPage(): Omit<goToPageActions.GoToPageActions, 'drive'> {
return goToPageActions.goToPageActions(this.step.bind(this))
}
/** Actions related to context menus. */
get contextMenu() {
return contextMenuActions.contextMenuActions(this.step.bind(this))
}
/** Switch to a different category. */
get goToCategory() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: DrivePageActions = this
return {
/** Switch to the "cloud" category. */
cloud() {
return self.step('Go to "Cloud" category', (page) =>
page.getByRole('button', { name: 'Cloud' }).getByText('Cloud').click(),
)
},
/** Switch to the "local" category. */
local() {
return self.step('Go to "Local" category', (page) =>
page.getByRole('button', { name: 'Local' }).getByText('Local').click(),
)
},
/** Switch to the "recent" category. */
recent() {
return self.step('Go to "Recent" category', (page) =>
page.getByRole('button', { name: 'Recent' }).getByText('Recent').click(),
)
},
/** Switch to the "trash" category. */
trash() {
return self.step('Go to "Trash" category', (page) =>
page.getByRole('button', { name: 'Trash' }).getByText('Trash').click(),
)
},
}
}
/** Actions specific to the Drive table. */
get driveTable() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: DrivePageActions = this
return {
/** Click the column heading for the "name" column to change its sort order. */
clickNameColumnHeading() {
return self.step('Click "name" column heading', (page) =>
page.getByLabel('Sort by name').or(page.getByLabel('Stop sorting by name')).click(),
)
},
/** Click the column heading for the "modified" column to change its sort order. */
clickModifiedColumnHeading() {
return self.step('Click "modified" column heading', (page) =>
page
.getByLabel('Sort by modification date')
.or(page.getByLabel('Stop sorting by modification date'))
.click(),
)
},
/** Click to select a specific row. */
clickRow(index: number) {
return self.step(`Click drive table row #${index}`, (page) =>
locateAssetRows(page).nth(index).click({ position: actions.ASSET_ROW_SAFE_POSITION }),
)
},
/** Right click a specific row to bring up its context menu, or the context menu for multiple
* assets when right clicking on a selected asset when multiple assets are selected. */
rightClickRow(index: number) {
return self.step(`Right click drive table row #${index}`, (page) =>
locateAssetRows(page)
.nth(index)
.click({ button: 'right', position: actions.ASSET_ROW_SAFE_POSITION }),
)
},
/** Double click a row. */
doubleClickRow(index: number) {
return self.step(`Double dlick drive table row #${index}`, (page) =>
locateAssetRows(page).nth(index).dblclick({ position: actions.ASSET_ROW_SAFE_POSITION }),
)
},
/** Interact with the set of all rows in the Drive table. */
withRows(callback: baseActions.LocatorCallback) {
return self.step('Interact with drive table rows', async (page) => {
await callback(locateAssetRows(page))
})
},
/** Drag a row onto another row. */
dragRowToRow(from: number, to: number) {
return self.step(`Drag drive table row #${from} to row #${to}`, async (page) => {
const rows = locateAssetRows(page)
await rows.nth(from).dragTo(rows.nth(to), {
sourcePosition: ASSET_ROW_SAFE_POSITION,
targetPosition: ASSET_ROW_SAFE_POSITION,
})
})
},
/** Drag a row onto another row. */
dragRow(from: number, to: test.Locator, force?: boolean) {
return self.step(`Drag drive table row #${from} to custom locator`, (page) =>
locateAssetRows(page)
.nth(from)
.dragTo(to, {
sourcePosition: ASSET_ROW_SAFE_POSITION,
...(force == null ? {} : { force }),
}),
)
},
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets to show. */
expectPlaceholderRow() {
return self.step('Expect placeholder row', async (page) => {
const rows = locateAssetRows(page)
await test.expect(rows).toHaveCount(1)
await test.expect(rows).toHaveText(/You have no files/)
})
},
/** A test assertion to confirm that there is only one row visible, and that row is the
* placeholder row displayed when there are no assets in Trash. */
expectTrashPlaceholderRow() {
return self.step('Expect trash placeholder row', async (page) => {
const rows = locateAssetRows(page)
await test.expect(rows).toHaveCount(1)
await test.expect(rows).toHaveText(/Your trash is empty/)
})
},
/** Toggle a column's visibility. */
get toggleColumn() {
return {
/** Toggle visibility for the "modified" column. */
modified() {
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Modified').click(),
)
},
/** Toggle visibility for the "shared with" column. */
sharedWith() {
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Shared With').click(),
)
},
/** Toggle visibility for the "labels" column. */
labels() {
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Labels').click(),
)
},
/** Toggle visibility for the "accessed by projects" column. */
accessedByProjects() {
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Accessed By Projects').click(),
)
},
/** Toggle visibility for the "accessed data" column. */
accessedData() {
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Accessed Data').click(),
)
},
/** Toggle visibility for the "docs" column. */
docs() {
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Docs').click(),
)
},
}
},
}
}
/** Open the "start" modal. */
openStartModal() {
return this.step('Open "start" modal', (page) =>
page.getByText('Start with a template').click(),
).into(StartModalActions)
}
/** Create a new empty project. */
newEmptyProject() {
return this.step('Create empty project', (page) =>
page.getByText('New Empty Project').click(),
).into(EditorPageActions)
}
/** Interact with the drive view (the main container of this page). */
withDriveView(callback: baseActions.LocatorCallback) {
return this.step('Interact with drive view', (page) => callback(actions.locateDriveView(page)))
}
/** Create a new folder using the icon in the Drive Bar. */
createFolder() {
return this.step('Create folder', (page) =>
Dashboard improvements (#10715) - Frontend part of https://github.com/enso-org/cloud-v2/issues/1397 - Show organization details to everyone (behavior unchanged) - :warning: Allow editing only for admins - :warning: Currently there is no backend endpoint to get organization permissions - Stop (incorrectly) submitting *all* settings inputs twice - Frontend part of https://github.com/enso-org/cloud-v2/issues/1396 - Fix "remove invitation" sending wrong request - Stop sending `organizationId` in "create invitation" request - Not adding `email` autocomplete to `/registration` - Currently already exists - but it will need to be revisited after the new sign up flow PR is merged. - Fix https://github.com/enso-org/cloud-v2/issues/1407 - Fix project open request being sent multiple times - Address https://github.com/enso-org/enso/issues/10633#issuecomment-2252540802 - Fix path to local projects (previously gave the path to their containing folder Other fixes: - Various fixes for autocomplete: - Fix autocomplete appearance (dropdown is no longer detached from main input) - Add tooltips for overflowing autocomplete entries - Add tooltips for overflowing usernames in "manage permissions" modal - Animate height of "asset search bar" dropdown and "autocomplete" dropdown - Auto-size names of object keys in Datalink input Other changes: - Avoid gap with missing background on right side of tab bar when resizing window due to the clip path being animated - Add <kbd>Cmd</kbd>+<kbd>W</kbd> and <kbd>Cmd</kbd>+<kbd>Option</kbd>+<kbd>W</kbd> to close tab - Make <kbd>Escape</kbd> only close tab if it is the Settings tab (a temporary tab) # Important Notes None
2024-08-01 14:29:05 +03:00
page.getByRole('button', { name: 'New Folder', exact: true }).click(),
)
}
/** Upload a file using the icon in the Drive Bar. */
uploadFile(
name: string,
contents: WithImplicitCoercion<Uint8Array | string | readonly number[]>,
mimeType = 'text/plain',
) {
return this.step(`Upload file '${name}'`, async (page) => {
const fileChooserPromise = page.waitForEvent('filechooser')
Offline Mode Support (#10317) #### Tl;dr Closes: enso-org/cloud-v2#1283 This PR significantly reimplements Offline mode <details><summary>Demo Presentation</summary> <p> https://github.com/enso-org/enso/assets/61194245/752d0423-9c0a-43ba-91e3-4a6688f77034 </p> </details> --- #### Context: Offline mode is one of the core features of the dashboard. Unfortunately, after adding new features and a few refactoring, we lost the ability to work offline. This PR should bring this functionality back, with a few key differences: 1. We require users to sign in before using the dashboard even in local mode. 2. Once a user is logged in, we allow him to work with local files 3. If a user closes the dashboard, and then open it, he can continue using it in offline mode #### This Change: What does this change do in the larger context? Specific details to highlight for review: 1. Reimplements `<AuthProvider />` functionality, now it implemented on top of `<Suspense />` and ReactQuery 2. Reimplements Backend module flow, now remote backend is always created, You no longer need to check if the RemoteBackend is present 3. Introduces new `<Suspense />` component, which is aware of offline status 4. Introduce new offline-related hooks 5. Add a banner to the form if it's unable to submit it offline 6. Refactor `InviteUserDialog` to the new `<Form />` component 7. Fixes redirect bug when the app doesn't redirect a user to the dashboard after logging in 8. Fixes strange behavior when `/users/me` could stuck into infinite refetch 9. Redesign the Cloud table for offline mode. 10. Adds blocking UI dialog when a user clicks "log out" button #### Test Plan: This PR requires thorough QA on the login flow across the browser and IDE. All redirect logic must stay unchanged. ---
2024-06-21 10:14:40 +03:00
await page.getByRole('button', { name: 'Import' }).click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles([{ name, buffer: Buffer.from(contents), mimeType }])
})
}
/** Create a new secret using the icon in the Drive Bar. */
createSecret(name: string, value: string) {
return this.step(`Create secret '${name}' = '${value}'`, async (page) => {
await actions.locateNewSecretIcon(page).click()
await actions.locateSecretNameInput(page).fill(name)
await actions.locateSecretValueInput(page).fill(value)
await actions.locateCreateButton(page).click()
})
}
/** Toggle the Asset Panel open or closed. */
toggleAssetPanel() {
return this.step('Toggle asset panel', (page) =>
page.getByLabel('Asset Panel').locator('visible=true').click(),
)
}
/** Interact with the container element of the assets table. */
withAssetsTable(callback: baseActions.LocatorCallback) {
return this.step('Interact with drive table', async (page) => {
await callback(actions.locateAssetsTable(page))
})
}
/** Interact with the Asset Panel. */
withAssetPanel(callback: baseActions.LocatorCallback) {
return this.step('Interact with asset panel', async (page) => {
await callback(actions.locateAssetPanel(page))
})
}
/** Open the Data Link creation modal by clicking on the Data Link icon. */
openDataLinkModal() {
return this.step('Open "new data link" modal', (page) =>
Offline Mode Support (#10317) #### Tl;dr Closes: enso-org/cloud-v2#1283 This PR significantly reimplements Offline mode <details><summary>Demo Presentation</summary> <p> https://github.com/enso-org/enso/assets/61194245/752d0423-9c0a-43ba-91e3-4a6688f77034 </p> </details> --- #### Context: Offline mode is one of the core features of the dashboard. Unfortunately, after adding new features and a few refactoring, we lost the ability to work offline. This PR should bring this functionality back, with a few key differences: 1. We require users to sign in before using the dashboard even in local mode. 2. Once a user is logged in, we allow him to work with local files 3. If a user closes the dashboard, and then open it, he can continue using it in offline mode #### This Change: What does this change do in the larger context? Specific details to highlight for review: 1. Reimplements `<AuthProvider />` functionality, now it implemented on top of `<Suspense />` and ReactQuery 2. Reimplements Backend module flow, now remote backend is always created, You no longer need to check if the RemoteBackend is present 3. Introduces new `<Suspense />` component, which is aware of offline status 4. Introduce new offline-related hooks 5. Add a banner to the form if it's unable to submit it offline 6. Refactor `InviteUserDialog` to the new `<Form />` component 7. Fixes redirect bug when the app doesn't redirect a user to the dashboard after logging in 8. Fixes strange behavior when `/users/me` could stuck into infinite refetch 9. Redesign the Cloud table for offline mode. 10. Adds blocking UI dialog when a user clicks "log out" button #### Test Plan: This PR requires thorough QA on the login flow across the browser and IDE. All redirect logic must stay unchanged. ---
2024-06-21 10:14:40 +03:00
page.getByRole('button', { name: 'New Datalink' }).click(),
).into(NewDataLinkModalActions)
}
/** Interact with the context menus (the context menus MUST be visible). */
withContextMenus(callback: baseActions.LocatorCallback) {
return this.step('Interact with context menus', async (page) => {
await callback(actions.locateContextMenus(page))
})
}
}