2024-06-05 20:46:32 +01:00
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth" ;
import { expect , Frame , Locator , Page } from "@playwright/test" ;
import crypto from "crypto" ;
export function generateUniqueName ( baseName = "" , length = 4 ) : string {
return ` ${ baseName } ${ crypto . randomBytes ( length ) . toString ( "hex" ) } ` ;
}
export function generateDatabaseNameWithTimestamp ( baseName = "db" , length = 1 ) : string {
// We use '_' as the separator because it's supported across all the API types.
return ` ${ baseName } ${ crypto . randomBytes ( length ) . toString ( "hex" ) } _ ${ Date . now ( ) } ` ;
}
export async function getAzureCLICredentials ( ) : Promise < AzureCliCredentials > {
return await AzureCliCredentials . create ( ) ;
}
export async function getAzureCLICredentialsToken ( ) : Promise < string > {
const credentials = await getAzureCLICredentials ( ) ;
const token = ( await credentials . getToken ( ) ) . accessToken ;
return token ;
}
export enum TestAccount {
Tables = "Tables" ,
Cassandra = "Cassandra" ,
Gremlin = "Gremlin" ,
Mongo = "Mongo" ,
Mongo32 = "Mongo32" ,
SQL = "SQL" ,
}
export const defaultAccounts : Record < TestAccount , string > = {
[ TestAccount . Tables ] : "portal-tables-runner" ,
[ TestAccount . Cassandra ] : "portal-cassandra-runner" ,
[ TestAccount . Gremlin ] : "portal-gremlin-runner" ,
[ TestAccount . Mongo ] : "portal-mongo-runner" ,
[ TestAccount . Mongo32 ] : "portal-mongo32-runner" ,
[ TestAccount . SQL ] : "portal-sql-runner-west-us" ,
} ;
export const resourceGroupName = process . env . DE_TEST_RESOURCE_GROUP ? ? "runners" ;
export const subscriptionId = process . env . DE_TEST_SUBSCRIPTION_ID ? ? "69e02f2d-f059-4409-9eac-97e8a276ae2c" ;
function tryGetStandardName ( accountType : TestAccount ) {
if ( process . env . DE_TEST_ACCOUNT_PREFIX ) {
const actualPrefix = process . env . DE_TEST_ACCOUNT_PREFIX . endsWith ( "-" )
? process . env . DE_TEST_ACCOUNT_PREFIX
: ` ${ process . env . DE_TEST_ACCOUNT_PREFIX } - ` ;
return ` ${ actualPrefix } ${ accountType . toLocaleLowerCase ( ) } ` ;
}
}
export function getAccountName ( accountType : TestAccount ) {
return (
process . env [ ` DE_TEST_ACCOUNT_NAME_ ${ accountType . toLocaleUpperCase ( ) } ` ] ? ?
tryGetStandardName ( accountType ) ? ?
defaultAccounts [ accountType ]
) ;
}
export async function getTestExplorerUrl ( accountType : TestAccount , iframeSrc? : string ) : Promise < string > {
// We can't retrieve AZ CLI credentials from the browser so we get them here.
const token = await getAzureCLICredentialsToken ( ) ;
const accountName = getAccountName ( accountType ) ;
2024-08-01 18:02:36 +01:00
const params = new URLSearchParams ( ) ;
params . set ( "accountName" , accountName ) ;
params . set ( "resourceGroup" , resourceGroupName ) ;
params . set ( "subscriptionId" , subscriptionId ) ;
params . set ( "token" , token ) ;
// There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example)
// For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false.
params . set ( "feature.enableCopilot" , "false" ) ;
2024-06-05 20:46:32 +01:00
if ( iframeSrc ) {
2024-08-01 18:02:36 +01:00
params . set ( "iframeSrc" , iframeSrc ) ;
2024-06-05 20:46:32 +01:00
}
2024-08-01 18:02:36 +01:00
return ` https://localhost:1234/testExplorer.html? ${ params . toString ( ) } ` ;
2024-06-05 20:46:32 +01:00
}
/** Helper class that provides locator methods for TreeNode elements, on top of a Locator */
class TreeNode {
constructor (
public element : Locator ,
public frame : Frame ,
public id : string ,
) { }
async openContextMenu ( ) : Promise < void > {
await this . element . click ( { button : "right" } ) ;
}
contextMenuItem ( name : string ) : Locator {
return this . frame . getByTestId ( ` TreeNode/ContextMenuItem: ${ name } ` ) ;
}
async expand ( ) : Promise < void > {
// Sometimes, the expand button doesn't load at all, because the node didn't have children when it was initially loaded.
// Still, clicking the node will trigger loading and expansion. So if the node isn't expanded, we click it.
// The "aria-expanded" attribute is applied to the TreeItem. But we have the TreeItemLayout selected because the TreeItem contains the child tree as well.
// So, we need to find the TreeItem that contains this TreeItemLayout.
const treeNodeContainer = this . frame . getByTestId ( ` TreeNodeContainer: ${ this . id } ` ) ;
if ( ( await treeNodeContainer . getAttribute ( "aria-expanded" ) ) !== "true" ) {
// Click the node, to trigger loading and expansion
await this . element . click ( ) ;
}
await expect ( treeNodeContainer ) . toHaveAttribute ( "aria-expanded" , "true" ) ;
}
}
/** Helper class that provides locator methods for DataExplorer components, on top of a Frame */
export class DataExplorer {
constructor ( public frame : Frame ) { }
2024-08-01 18:02:36 +01:00
/ * * S e l e c t t h e p r i m a r y g l o b a l c o m m a n d b u t t o n .
*
* There 's only a single "primary" button, but we still require you to pass the label to confirm you' re selecting the right button .
* /
globalCommandButton ( label : string ) : Locator {
return this . frame . getByTestId ( "GlobalCommands" ) . getByText ( label ) ;
}
/** Select the command bar button with the specified label */
2024-06-05 20:46:32 +01:00
commandBarButton ( label : string ) : Locator {
return this . frame . getByTestId ( ` CommandBar/Button: ${ label } ` ) . and ( this . frame . locator ( "css=button" ) ) ;
}
2024-08-01 18:02:36 +01:00
/** Select the side panel with the specified title */
2024-06-05 20:46:32 +01:00
panel ( title : string ) : Locator {
return this . frame . getByTestId ( ` Panel: ${ title } ` ) ;
}
2024-08-01 18:02:36 +01:00
/** Select the tree node with the specified id */
2024-06-05 20:46:32 +01:00
treeNode ( id : string ) : TreeNode {
return new TreeNode ( this . frame . getByTestId ( ` TreeNode: ${ id } ` ) , this . frame , id ) ;
}
2024-08-01 18:02:36 +01:00
/** Waits for the panel with the specified title to be open, then runs the provided callback. After the callback completes, waits for the panel to close. */
2024-06-05 20:46:32 +01:00
async whilePanelOpen ( title : string , action : ( panel : Locator , okButton : Locator ) = > Promise < void > ) : Promise < void > {
const panel = this . panel ( title ) ;
await panel . waitFor ( ) ;
const okButton = panel . getByTestId ( "Panel/OkButton" ) ;
await action ( panel , okButton ) ;
await panel . waitFor ( { state : "detached" } ) ;
}
2024-08-01 18:02:36 +01:00
/** Waits for the Data Explorer app to load */
2024-06-05 20:46:32 +01:00
static async waitForExplorer ( page : Page ) {
const iframeElement = await page . getByTestId ( "DataExplorerFrame" ) . elementHandle ( ) ;
if ( iframeElement === null ) {
throw new Error ( "Explorer iframe not found" ) ;
}
const explorerFrame = await iframeElement . contentFrame ( ) ;
if ( explorerFrame === null ) {
throw new Error ( "Explorer frame not found" ) ;
}
await explorerFrame ? . getByTestId ( "DataExplorerRoot" ) . waitFor ( ) ;
return new DataExplorer ( explorerFrame ) ;
}
2024-08-01 18:02:36 +01:00
/** Opens the Data Explorer app using the specified test account (and optionally, the provided IFRAME src url). */
2024-06-05 20:46:32 +01:00
static async open ( page : Page , testAccount : TestAccount , iframeSrc? : string ) : Promise < DataExplorer > {
const url = await getTestExplorerUrl ( testAccount , iframeSrc ) ;
await page . goto ( url ) ;
return DataExplorer . waitForExplorer ( page ) ;
}
}