Fix E2E tests. Add Playwright (#698)

This commit is contained in:
Steve Faulkner 2021-04-19 22:08:25 -05:00 committed by GitHub
parent 914e969083
commit 2fd6305944
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 3818 additions and 1494 deletions

View File

@ -126,58 +126,22 @@ jobs:
with:
name: screenshots
path: failed-*
accessibility:
name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Accessibility Check
run: |
# Ubuntu gets mad when webpack runs too many files watchers
cat /proc/sys/fs/inotify/max_user_watches
sudo sysctl fs.inotify.max_user_watches=524288
sudo sysctl -p
npm ci
npm start &
npx wait-on -i 5000 https-get://0.0.0.0:1234/
node utils/accesibilityCheck.js
shell: bash
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendhosted:
name: "End to End Tests"
endtoend:
name: "E2E"
needs: [cleanupaccounts]
runs-on: ubuntu-latest
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT }}
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy:
fail-fast: false
matrix:
test-file:
- ./test/cassandra/container.spec.ts
- ./test/mongo/mongoIndexPolicy.spec.ts
- ./test/notebooks/uploadAndOpenNotebook.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
- ./test/sql/container.spec.ts
- ./test/mongo/container.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
steps:
@ -188,16 +152,15 @@ jobs:
node-version: 14.x
- run: npm ci
- run: npm start &
- run: node utils/cleanupDBs.js
- run: npm run wait-for-server
- name: ${{ matrix['test-file'] }}
run: npx jest -c ./jest.config.e2e.js --detectOpenHandles ${{ matrix['test-file'] }}
run: npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }}
shell: bash
- uses: actions/upload-artifact@v2
if: failure()
with:
name: screenshots
path: failed-*
path: screenshots/
cleanupaccounts:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest

4
.gitignore vendored
View File

@ -14,4 +14,6 @@ Contracts/*
.DS_Store
.cache/
.env
failure.png
failure.png
screenshots/*
GettingStarted-ignore*.ipynb

View File

@ -153,7 +153,7 @@ Cosmos Explorer has been under constant development for over 5 years. As a resul
✅ DO
- Use [Puppeteer](https://developers.google.com/web/tools/puppeteer) and [Jest](https://jestjs.io/)
- Use [Playwright](https://github.com/microsoft/playwright) and [Jest](https://jestjs.io/)
- Write or modify an existing E2E test that covers the primary use case of any major feature.
- Use caution. Do not try to cover every case. End to End tests can be slow and brittle.

17
jest-playwright.config.js Normal file
View File

@ -0,0 +1,17 @@
const isCI = require("is-ci");
module.exports = {
exitOnPageError: false,
launchOptions: {
headless: isCI,
slowMo: 10,
timeout: 60000,
},
contextOptions: {
ignoreHTTPSErrors: true,
viewport: {
width: 1920,
height: 1080,
},
},
};

View File

@ -1,12 +0,0 @@
const isCI = require("is-ci");
module.exports = {
launch: {
headless: isCI,
slowMo: 55,
defaultViewport: null,
ignoreHTTPSErrors: true,
args: ["--disable-web-security"],
exitOnPageError: false,
},
};

View File

@ -1,5 +0,0 @@
module.exports = {
preset: "jest-puppeteer",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
};

View File

@ -0,0 +1,7 @@
module.exports = {
preset: "jest-playwright-preset",
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
setupFiles: ["dotenv/config"],
testEnvironment: "./test/playwrightEnv.js",
setupFilesAfterEnv: ["expect-playwright"],
};

3946
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -111,15 +111,12 @@
"@types/d3": "5.9.2",
"@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/expect-puppeteer": "4.4.5",
"@types/hasher": "0.0.31",
"@types/jest": "26.0.20",
"@types/jest-environment-puppeteer": "4.4.1",
"@types/memoize-one": "4.1.1",
"@types/node": "12.11.1",
"@types/promise.prototype.finally": "2.0.3",
"@types/prop-types": "15.5.8",
"@types/puppeteer": "5.4.3",
"@types/q": "1.5.1",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.3",
@ -131,7 +128,6 @@
"@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1",
"axe-puppeteer": "1.1.0",
"babel-jest": "24.9.0",
"babel-loader": "8.1.0",
"buffer": "5.1.0",
@ -146,6 +142,7 @@
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react-hooks": "4.2.0",
"expect-playwright": "0.3.3",
"expose-loader": "0.7.5",
"fast-glob": "3.2.5",
"file-loader": "2.0.0",
@ -155,7 +152,7 @@
"html-webpack-plugin": "3.2.0",
"jest": "25.5.4",
"jest-canvas-mock": "2.1.0",
"jest-puppeteer": "4.4.0",
"jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7",
"less": "3.8.1",
"less-loader": "4.1.0",
@ -163,8 +160,8 @@
"mini-css-extract-plugin": "0.4.3",
"monaco-editor-webpack-plugin": "1.7.0",
"node-fetch": "2.6.1",
"playwright": "1.10.0",
"prettier": "2.2.1",
"puppeteer": "8.0.0",
"raw-loader": "0.5.1",
"rimraf": "3.0.0",
"sinon": "3.2.1",

View File

@ -1,148 +1,35 @@
import "expect-puppeteer";
import { Frame } from "puppeteer";
import { generateUniqueName, login } from "../utils/shared";
import { jest } from "@jest/globals";
import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared";
jest.setTimeout(120000);
jest.setTimeout(300000);
const RENDER_DELAY = 800;
const RETRY_DELAY = 5000;
const CREATE_DELAY = 10000;
const LOADING_STATE_DELAY = 2500;
test("Cassandra keyspace and table CRUD", async () => {
const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table");
describe("Collection Add and Delete Cassandra spec", () => {
it("creates a collection", async () => {
try {
const keyspaceId = generateUniqueName("key");
const tableId = generateUniqueName("tab");
const frame = await login(process.env.CASSANDRA_CONNECTION_STRING);
// create new table
await frame.waitFor('button[data-test="New Table"]', { visible: true });
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Table"]');
// type keyspace id
await frame.waitFor('input[id="keyspace-id"]', { visible: true });
await frame.type('input[id="keyspace-id"]', keyspaceId);
// type table id
await frame.waitFor('input[class="textfontclr"]');
await frame.type('input[class="textfontclr"]', tableId);
// click submit
await frame.waitFor("#cassandraaddcollectionpane > div > form > div.paneFooter > div > input");
await frame.click("#cassandraaddcollectionpane > div > form > div.paneFooter > div > input");
// open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
const selectedDbId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, databases[0]);
await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true };
await frame.waitFor(CREATE_DELAY);
await frame.waitFor("div[class='rowData'] > span[class='message']");
const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => {
return elements.some((el) => el.textContent.includes("Successfully created"));
});
expect(didCreateContainer).toBe(true);
await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await clickDBMenu(selectedDbId, frame);
const collections = await frame.$$(
`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`
);
if (collections.length) {
await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, {
visible: true,
});
const textId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, collections[0]);
await frame.waitFor(`div[data-test="${textId}"]`, { visible: true });
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true });
await frame.click(`div[data-test="${textId}"] > div > button`);
// click delete container
await frame.waitFor(RENDER_DELAY);
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
// confirm delete container
await frame.waitFor('input[id="confirmCollectionId"]', { visible: true });
await frame.type('input[id="confirmCollectionId"]', textId);
// click delete
await frame.waitFor('button[id="sidePanelOkButton"]', { visible: true });
await frame.click('button[id="sidePanelOkButton"]');
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${textId}"]`);
}
// click context menu for database
await frame.waitFor(`div[data-test="${keyspaceId}"] > div > button`);
await frame.waitFor(RENDER_DELAY);
const button = await frame.$(`div[data-test="${keyspaceId}"] > div > button`);
await button.focus();
await button.asElement().click();
// click delete database
await frame.waitFor(RENDER_DELAY);
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await dbElements[0].click();
// confirm delete database
await frame.waitForSelector('input[id="confirmDatabaseId"]', { visible: true });
await frame.waitFor(RENDER_DELAY);
await frame.type('input[id="confirmDatabaseId"]', keyspaceId.trim());
// click delete
await frame.click('button[id="sidePanelOkButton"]');
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${keyspaceId}"]`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `failed-${testName}.jpg` });
throw error;
}
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
await explorer.click('[data-test="New Table"]');
await explorer.click('[data-test="addCollection-keyspaceId"]');
await explorer.fill('[data-test="addCollection-keyspaceId"]', keyspaceId);
await explorer.click('[data-test="addCollection-tableId"]');
await explorer.fill('[data-test="addCollection-tableId"]', tableId);
await explorer.click('[aria-label="Add Table"] [data-test="addCollection-createCollection"]');
await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`);
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
await explorer.click('[aria-label="Submit"]');
await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', keyspaceId);
await explorer.click("#sidePanelOkButton");
await expect(explorer).not.toHaveText(".dataResourceTree", keyspaceId);
await expect(explorer).not.toHaveText(".dataResourceTree", tableId);
});
async function clickDBMenu(dbId: string, frame: Frame, retries = 0) {
const button = await frame.$(`div[data-test="${dbId}"]`);
await button.focus();
const handler = await button.asElement();
await handler.click();
await ensureMenuIsOpen(dbId, frame, retries);
return button;
}
async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) {
await frame.waitFor(RETRY_DELAY);
const button = await frame.$(`div[data-test="${dbId}"]`);
const classList = await frame.evaluate((button) => {
return button.parentElement.classList;
}, button);
if (!Object.values(classList).includes("selected") && retries < 5) {
retries = retries + 1;
await clickDBMenu(dbId, frame, retries);
}
}

View File

@ -1,164 +1,53 @@
import "expect-puppeteer";
import { Frame } from "puppeteer";
import { generateDatabaseName, generateUniqueName, login } from "../utils/shared";
import { jest } from "@jest/globals";
import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared";
jest.setTimeout(240000);
jest.setTimeout(300000);
const LOADING_STATE_DELAY = 2500;
const RETRY_DELAY = 5000;
const CREATE_DELAY = 10000;
const RENDER_DELAY = 1000;
test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
describe("Collection Add and Delete Mongo spec", () => {
it("creates a collection", async () => {
try {
const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col");
const sharedKey = `${generateUniqueName()}`;
const frame = await login(process.env.MONGO_CONNECTION_STRING);
// create new collection
await frame.waitFor('button[data-test="New Collection"]', { visible: true });
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Collection"]');
// check new database
await frame.waitFor('input[data-test="addCollection-createNewDatabase"]');
await frame.click('input[data-test="addCollection-createNewDatabase"]');
// check shared throughput
await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]');
await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]');
// type database id
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]');
await dbInput.press("Backspace");
await dbInput.type(dbId);
// type collection id
await frame.waitFor('input[data-test="addCollection-collectionId"]');
const input = await frame.$('input[data-test="addCollection-collectionId"]');
await input.press("Backspace");
await input.type(collectionId);
// type partition key value
await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]');
const keyInput = await frame.$('input[data-test="addCollection-partitionKeyValue"]');
await keyInput.press("Backspace");
await keyInput.type(sharedKey);
// click submit
await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection");
// validate created
// open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
const selectedDbId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, databases[0]);
await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true };
await frame.waitFor(CREATE_DELAY);
await frame.waitFor("div[class='rowData'] > span[class='message']");
const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => {
return elements.some((el) => el.textContent.includes("Successfully created"));
});
expect(didCreateContainer).toBe(true);
await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await clickDBMenu(selectedDbId, frame);
const collections = await frame.$$(
`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`
);
if (collections.length) {
const textId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, collections[0]);
await frame.waitFor(`div[data-test="${textId}"]`, { visible: true });
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true });
await frame.click(`div[data-test="${textId}"] > div > button`);
// click delete container
await frame.waitFor(RENDER_DELAY);
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
// confirm delete container
await frame.waitFor('input[id="confirmCollectionId"]', { visible: true });
await frame.type('input[id="confirmCollectionId"]', textId);
// click delete
await frame.waitFor('button[id="sidePanelOkButton"]', { visible: true });
await frame.click('button[id="sidePanelOkButton"]');
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${textId}"]`);
}
// click context menu for database
await frame.waitFor(`div[data-test="${selectedDbId}"] > div > button`);
await frame.waitFor(RENDER_DELAY);
const button = await frame.$(`div[data-test="${selectedDbId}"] > div > button`);
await button.focus();
await button.asElement().click();
// click delete database
await frame.waitFor(RENDER_DELAY);
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
// confirm delete database
await frame.waitForSelector('input[id="confirmDatabaseId"]', { visible: true });
await frame.waitFor(RENDER_DELAY);
await frame.type('input[id="confirmDatabaseId"]', selectedDbId);
// click delete
await frame.click('button[id="sidePanelOkButton"]');
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${selectedDbId}"]`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `failed-${testName}.jpg` });
throw error;
}
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
// Create new database and collection
await explorer.click('[data-test="New Collection"]');
await explorer.click('[data-test="addCollection-newDatabaseId"]');
await explorer.fill('[data-test="addCollection-newDatabaseId"]', databaseId);
await explorer.click('[data-test="addCollection-collectionId"]');
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
await explorer.click('[data-test="addCollection-collectionId"]');
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
await explorer.click('[data-test="addCollection-partitionKeyValue"]');
await explorer.fill('[data-test="addCollection-partitionKeyValue"]', "/pk");
await explorer.click('[data-test="addCollection-createCollection"]');
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`);
// Create indexing policy
await safeClick(explorer, ".nodeItem >> text=Settings");
await explorer.click('button[role="tab"]:has-text("Indexing Policy")');
await explorer.click('[aria-label="Index Field Name 0"]');
await explorer.fill('[aria-label="Index Field Name 0"]', "foo");
await explorer.click("text=Select an index type");
await explorer.click('button[role="option"]:has-text("Single Field")');
await explorer.click('[data-test="Save"]');
// Remove indexing policy
await explorer.click('[aria-label="Delete index Button"]');
await explorer.click('[data-test="Save"]');
// Delete database and collection
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")');
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
await explorer.click("#sidePanelOkButton");
await expect(explorer).not.toHaveText(".dataResourceTree", databaseId);
await expect(explorer).not.toHaveText(".dataResourceTree", containerId);
});
async function clickDBMenu(dbId: string, frame: Frame, retries = 0) {
const button = await frame.$(`div[data-test="${dbId}"]`);
await button.focus();
const handler = await button.asElement();
await handler.click();
await ensureMenuIsOpen(dbId, frame, retries);
return button;
}
async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) {
await frame.waitFor(RETRY_DELAY);
const button = await frame.$(`div[data-test="${dbId}"]`);
const classList = await frame.evaluate((button) => {
return button.parentElement.classList;
}, button);
if (!Object.values(classList).includes("selected") && retries < 5) {
retries = retries + 1;
await clickDBMenu(dbId, frame, retries);
}
}

View File

@ -1,106 +0,0 @@
import "expect-puppeteer";
import { createDatabase, generateUniqueName, onClickSaveButton } from "../utils/shared";
const LOADING_STATE_DELAY = 5000;
jest.setTimeout(300000);
describe("MongoDB Index policy tests", () => {
it("Open, Create and Save Index", async () => {
try {
const singleFieldId = generateUniqueName("key");
const wildCardId = generateUniqueName("key") + "$**";
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
const dropDown = "Index Type ";
let index = 0;
//open dataBaseMenu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const { databaseId, collectionId } = await createDatabase(frame);
await frame.waitFor(25000);
// click on database
await frame.waitForSelector(`div[data-test="${databaseId}"]`);
await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${databaseId}"]`);
await frame.waitFor(LOADING_STATE_DELAY);
// click on scale & setting
await frame.waitFor(`div[data-test="${collectionId}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${collectionId}"]`);
await frame.waitFor(`div[data-test="Scale & Settings"]`), { visible: true };
await frame.waitFor(10000);
await frame.click(`div[data-test="Scale & Settings"]`);
await frame.waitFor(`button[data-content="Indexing Policy"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`button[data-content="Indexing Policy"]`);
// Type to single Field
let throughput = await frame.$$(".ms-TextField-field");
const selectedDropDownSingleField = dropDown + index;
await frame.waitFor(`div[aria-label="${selectedDropDownSingleField}"]`), { visible: true };
await throughput[index].type(singleFieldId);
await frame.click(`div[aria-label="${selectedDropDownSingleField}"]`);
await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`button[title="Single Field"]`);
index++;
// Type to wild card
throughput = await frame.$$(".ms-TextField-field");
await throughput[index].type(wildCardId);
const selectedDropDownWildCard = dropDown + index;
await frame.waitFor(`div[aria-label="${selectedDropDownWildCard}"]`), { visible: true };
await frame.click(`div[aria-label="${selectedDropDownWildCard}"]`);
await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`button[title="Wildcard"]`);
index++;
// click save Button
await onClickSaveButton(frame);
// check the array
let singleFieldIndexInserted = false,
wildCardIndexInserted = false;
await frame.waitFor("div[data-automationid='DetailsRowCell'] > span"), { visible: true };
await frame.waitFor(20000);
const elements = await frame.$$("div[data-automationid='DetailsRowCell'] > span");
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const text = await frame.evaluate((element) => element.textContent, element);
if (text.includes(wildCardId)) {
wildCardIndexInserted = true;
} else if (text.includes(singleFieldId)) {
singleFieldIndexInserted = true;
}
}
await frame.waitFor(20000);
expect(wildCardIndexInserted).toBe(true);
expect(singleFieldIndexInserted).toBe(true);
//delete all index policy
await frame.waitFor("button[aria-label='Delete index Button']"), { visible: true };
const deleteButton = await frame.$$("button[aria-label='Delete index Button']");
for (let i = 0; i < deleteButton.length; i++) {
await frame.click(`button[aria-label="Delete index Button"]`);
}
await onClickSaveButton(frame);
//check for cleaning
await frame.waitFor(20000);
await frame.waitFor("div[data-automationid='DetailsRowCell'] > span"), { visible: true };
const isDeletionComplete = await frame.$$("div[data-automationid='DetailsRowCell'] > span");
expect(isDeletionComplete).toHaveLength(2);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `failed-${testName}.jpg` });
throw error;
}
});
});

View File

@ -1,52 +0,0 @@
import { ElementHandle, Frame } from "puppeteer";
import * as path from "path";
export const NOTEBOOK_OPERATION_DELAY = 5000;
export const RENDER_DELAY = 2500;
export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: string): Promise<ElementHandle<Element>> => {
const notebookNode = await getNotebookNode(frame, notebookName);
if (notebookNode) {
return notebookNode;
}
const uploadNotebookPath = path.join(__dirname, "testNotebooks", notebookName);
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
const treeNodeHeadersBeforeUpload = await notebookResourceTree.$$(".treeNodeHeader");
const ellipses = await treeNodeHeadersBeforeUpload[2].$("button");
await ellipses.click();
await frame.waitFor(RENDER_DELAY);
const menuItems = await frame.$$(".ms-ContextualMenu-item");
await menuItems[4].click();
const uploadFileButton = await frame.waitForSelector("#importFileButton");
uploadFileButton.click();
const fileChooser = await page.waitForFileChooser();
fileChooser.accept([uploadNotebookPath]);
const submitButton = await frame.waitForSelector("#uploadFileButton");
await submitButton.click();
await frame.waitFor(NOTEBOOK_OPERATION_DELAY);
return await getNotebookNode(frame, notebookName);
};
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
await frame.waitFor(RENDER_DELAY);
let currentNotebookNode: ElementHandle<Element>;
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
for (let i = 1; i < treeNodeHeaders.length; i++) {
currentNotebookNode = treeNodeHeaders[i];
const nodeLabel = await currentNotebookNode.$eval(".nodeLabel", (element) => element.textContent);
if (nodeLabel === uploadNotebookName) {
return currentNotebookNode;
}
}
return undefined;
};

View File

@ -0,0 +1,27 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import fs from "fs";
import path from "path";
jest.setTimeout(240000);
const filename = "GettingStarted.ipynb";
const fileToUpload = `GettingStarted-ignore${Math.floor(Math.random() * 100000)}.ipynb`;
fs.copyFileSync(path.join(__dirname, filename), path.join(__dirname, fileToUpload));
test("Notebooks", async () => {
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
// Upload and Delete Notebook
await explorer.click('[data-test="My Notebooks"] [aria-label="More"]');
await explorer.click('button[role="menuitem"]:has-text("Upload File")');
await explorer.setInputFiles("#importFileInput", path.join(__dirname, fileToUpload));
await explorer.click('[aria-label="Submit"]');
await explorer.click(`[data-test="${fileToUpload}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete")');
await explorer.click('button:has-text("Delete")');
await expect(explorer).not.toHaveText(".notebookResourceTree", fileToUpload);
});

View File

@ -1,28 +0,0 @@
import { uploadNotebookIfNotExist } from "./notebookTestUtils";
jest.setTimeout(300000);
const notebookName = "GettingStarted.ipynb";
describe("Notebook UI tests", () => {
it("Upload, Open and Delete Notebook", async () => {
try {
await page.goto("https://localhost:1234/testExplorer.html");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
await frame.waitForSelector(".galleryHeader");
const uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName);
await uploadedNotebookNode.click();
await frame.waitForSelector(".tabNavText");
const tabTitle = await frame.$eval(".tabNavText", (element) => element.textContent);
expect(tabTitle).toEqual(notebookName);
const closeIcon = await frame.waitForSelector(".close-Icon");
await closeIcon.click();
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
throw error;
}
});
});

26
test/playwrightEnv.js Normal file
View File

@ -0,0 +1,26 @@
const PlaywrightEnvironment = require("jest-playwright-preset/lib/PlaywrightEnvironment").default;
class CustomEnvironment extends PlaywrightEnvironment {
async setup() {
await super.setup();
// Your setup
}
async teardown() {
// Your teardown
await super.teardown();
}
async handleTestEvent(event) {
if (event.name === "test_done" && event.test.errors.length > 0) {
const parentName = event.test.parent.name.replace(/\W/g, "-");
const specName = event.test.name.replace(/\W/g, "-");
await this.global.page.screenshot({
path: `screenshots/${parentName}_${specName}.png`,
});
}
}
}
module.exports = CustomEnvironment;

View File

@ -1,47 +1,36 @@
jest.setTimeout(300000);
test("Self Serve", async () => {
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
describe("Self Serve", () => {
it("Launch Self Serve Example", async () => {
try {
await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
// wait for refresh RP call to end
await page.waitForTimeout(10000);
// wait for refresh RP call to end
await frame.waitFor(10000);
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
await frame.waitForSelector("#description-text-display");
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
await frame.waitForSelector("#description-text-display");
const regions = await frame.waitForSelector("#regions-dropdown-input");
const regions = await frame.waitForSelector("#regions-dropdown-input");
const currentRegionsDescription = await frame.$$("#currentRegionText-text-display");
expect(currentRegionsDescription).toHaveLength(0);
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
expect(disabledLoggingToggle).toHaveLength(0);
const currentRegionsDescription = await frame.$$("#currentRegionText-text-display");
expect(currentRegionsDescription).toHaveLength(0);
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
expect(disabledLoggingToggle).toHaveLength(0);
await regions.click();
const regionsDropdownElement1 = await frame.waitForSelector("#regions-dropdown-input-list0");
await regionsDropdownElement1.click();
await regions.click();
const regionsDropdownElement1 = await frame.waitForSelector("#regions-dropdown-input-list0");
await regionsDropdownElement1.click();
await frame.waitForSelector("#currentRegionText-text-display");
disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
expect(disabledLoggingToggle).toHaveLength(1);
await frame.waitForSelector("#currentRegionText-text-display");
disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
expect(disabledLoggingToggle).toHaveLength(1);
await frame.waitForSelector("#accountName-textField-input");
await frame.waitForSelector("#accountName-textField-input");
const enableDbLevelThroughput = await frame.waitForSelector("#enableDbLevelThroughput-toggle-input");
const dbThroughput = await frame.$$("#dbThroughput-slider-input");
expect(dbThroughput).toHaveLength(0);
await enableDbLevelThroughput.click();
await frame.waitForSelector("#dbThroughput-slider-input");
const enableDbLevelThroughput = await frame.waitForSelector("#enableDbLevelThroughput-toggle-input");
const dbThroughput = await frame.$$("#dbThroughput-slider-input");
expect(dbThroughput).toHaveLength(0);
await enableDbLevelThroughput.click();
await frame.waitForSelector("#dbThroughput-slider-input");
await frame.waitForSelector("#collectionThroughput-spinner-input");
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
throw error;
}
});
await frame.waitForSelector("#collectionThroughput-spinner-input");
});

View File

@ -1,163 +1,39 @@
import "expect-puppeteer";
import { Frame } from "puppeteer";
import { generateDatabaseName, generateUniqueName } from "../utils/shared";
import { jest } from "@jest/globals";
import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared";
jest.setTimeout(120000);
jest.setTimeout(300000);
const LOADING_STATE_DELAY = 2500;
const RETRY_DELAY = 5000;
const CREATE_DELAY = 10000;
const RENDER_DELAY = 1000;
test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container");
describe("Collection Add and Delete SQL spec", () => {
it("creates a collection", async () => {
try {
const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col");
const sharedKey = `/skey${generateUniqueName()}`;
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
// create new collection
await frame.waitFor('button[data-test="New Container"]', { visible: true });
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Container"]');
// check new database
await frame.waitFor('input[data-test="addCollection-createNewDatabase"]');
await frame.click('input[data-test="addCollection-createNewDatabase"]');
// check shared throughput
await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]');
await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]');
// type database id
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]');
await dbInput.press("Backspace");
await dbInput.type(dbId);
// type collection id
await frame.waitFor('input[data-test="addCollection-collectionId"]');
const input = await frame.$('input[data-test="addCollection-collectionId"]');
await input.press("Backspace");
await input.type(collectionId);
// type partition key value
await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]');
const keyInput = await frame.$('input[data-test="addCollection-partitionKeyValue"]');
await keyInput.press("Backspace");
await keyInput.type(sharedKey);
// click submit
await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection");
// validate created
// open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(CREATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
const selectedDbId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, databases[0]);
await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true };
await frame.waitFor(CREATE_DELAY);
await frame.waitFor("div[class='rowData'] > span[class='message']");
await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await clickDBMenu(selectedDbId, frame);
const collections = await frame.$$(
`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`
);
if (collections.length) {
await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, {
visible: true,
});
const textId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, collections[0]);
await frame.waitFor(`div[data-test="${textId}"]`, { visible: true });
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true });
await frame.click(`div[data-test="${textId}"] > div > button`);
// click delete container
await frame.waitFor(RENDER_DELAY);
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
// confirm delete container
await frame.waitFor('input[id="confirmCollectionId"]', { visible: true });
await frame.type('input[id="confirmCollectionId"]', textId);
// click delete
await frame.waitFor("button.genericPaneSubmitBtn", { visible: true });
await frame.click("button.genericPaneSubmitBtn");
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${textId}"]`);
}
// click context menu for database
await frame.waitFor(`div[data-test="${selectedDbId}"] > div > button`);
await frame.waitFor(CREATE_DELAY);
const button = await frame.$(`div[data-test="${selectedDbId}"] > div > button`);
await button.focus();
await button.asElement().click();
// click delete database
await frame.waitFor(CREATE_DELAY);
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
// confirm delete database
await frame.waitForSelector('input[id="confirmDatabaseId"]', { visible: true });
await frame.waitFor(CREATE_DELAY);
await frame.type('input[id="confirmDatabaseId"]', selectedDbId);
// click delete
await frame.click('button[id="sidePanelOkButton"]');
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(CREATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${selectedDbId}"]`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `failed-${testName}.jpg` });
throw error;
}
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
await explorer.click('[data-test="New Container"]');
await explorer.click('[data-test="addCollection-newDatabaseId"]');
await explorer.fill('[data-test="addCollection-newDatabaseId"]', databaseId);
await explorer.click('[data-test="addCollection-collectionId"]');
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
await explorer.click('[data-test="addCollection-collectionId"]');
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
await explorer.click('[data-test="addCollection-partitionKeyValue"]');
await explorer.fill('[data-test="addCollection-partitionKeyValue"]', "/pk");
await explorer.click('[data-test="addCollection-createCollection"]');
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")');
await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
await explorer.click("#sidePanelOkButton");
await expect(explorer).not.toHaveText(".dataResourceTree", databaseId);
await expect(explorer).not.toHaveText(".dataResourceTree", containerId);
});
async function clickDBMenu(dbId: string, frame: Frame, retries = 0) {
const button = await frame.$(`div[data-test="${dbId}"]`);
await button.focus();
const handler = await button.asElement();
await handler.click();
await ensureMenuIsOpen(dbId, frame, retries);
return button;
}
async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) {
await frame.waitFor(RETRY_DELAY);
const button = await frame.$(`div[data-test="${dbId}"]`);
const classList = await frame.evaluate((button) => {
return button.parentElement.classList;
}, button);
if (!Object.values(classList).includes("selected") && retries < 5) {
retries = retries + 1;
await clickDBMenu(dbId, frame, retries);
}
}

View File

@ -1,83 +1,46 @@
/* eslint-disable jest/expect-expect */
import "expect-puppeteer";
import { Frame } from "puppeteer";
import { generateDatabaseName, generateUniqueName } from "../utils/shared";
import { CosmosClient, PermissionMode } from "@azure/cosmos";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, PermissionMode } from "@azure/cosmos";
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
import { jest } from "@jest/globals";
import "expect-playwright";
import { generateDatabaseName, generateUniqueName } from "../utils/shared";
jest.setTimeout(120000);
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4";
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners";
jest.setTimeout(300000);
const RETRY_DELAY = 5000;
const CREATE_DELAY = 10000;
describe("Collection Add and Delete SQL spec", () => {
it("creates a collection", async () => {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner");
const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col");
const client = new CosmosClient({
endpoint: account.documentEndpoint,
key: keys.primaryMasterKey,
});
const { database } = await client.databases.createIfNotExists({ id: dbId });
const { container } = await database.containers.createIfNotExists({ id: collectionId });
const { user } = await database.users.upsert({ id: "testUser" });
const { resource: containerPermission } = await user.permissions.upsert({
id: "partitionLevelPermission",
permissionMode: PermissionMode.All,
resource: container.url,
});
const resourceTokenConnectionString = `AccountEndpoint=${account.documentEndpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission._token}`;
try {
await page.goto(process.env.DATA_EXPLORER_ENDPOINT);
await page.waitFor("div > p.switchConnectTypeText", { visible: true });
await page.click("div > p.switchConnectTypeText");
await page.type("input[class='inputToken']", resourceTokenConnectionString);
await page.click("input[value='Connect']");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
// validate created
// open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(CREATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
expect(await frame.$(`span[title="${collectionId}"]`)).toBeDefined();
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `failed-${testName}.jpg` });
throw error;
}
test("Resource token", async () => {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner");
const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col");
const client = new CosmosClient({
endpoint: account.documentEndpoint,
key: keys.primaryMasterKey,
});
const { database } = await client.databases.createIfNotExists({ id: dbId });
const { container } = await database.containers.createIfNotExists({ id: collectionId });
const { user } = await database.users.upsert({ id: "testUser" });
const { resource: containerPermission } = await user.permissions.upsert({
id: "partitionLevelPermission",
permissionMode: PermissionMode.All,
resource: container.url,
});
const resourceTokenConnectionString = `AccountEndpoint=${account.documentEndpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission._token}`;
await page.goto("https://localhost:1234/hostedExplorer.html");
await page.waitForSelector("div > p.switchConnectTypeText");
await page.click("div > p.switchConnectTypeText");
await page.type("input[class='inputToken']", resourceTokenConnectionString);
await page.click("input[value='Connect']");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
await explorer.textContent(`css=.dataResourceTree >> "${collectionId}"`);
});
async function clickDBMenu(dbId: string, frame: Frame, retries = 0) {
const button = await frame.$(`div[data-test="${dbId}"]`);
await button.focus();
const handler = await button.asElement();
await handler.click();
await ensureMenuIsOpen(dbId, frame, retries);
return button;
}
async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) {
await frame.waitFor(RETRY_DELAY);
const button = await frame.$(`div[data-test="${dbId}"]`);
const classList = await frame.evaluate((button) => {
return button.parentElement.classList;
}, button);
if (!Object.values(classList).includes("selected") && retries < 5) {
retries = retries + 1;
await clickDBMenu(dbId, frame, retries);
}
}

View File

@ -1,111 +1,27 @@
import "expect-puppeteer";
import { Frame } from "puppeteer";
import { generateUniqueName, login } from "../utils/shared";
import { jest } from "@jest/globals";
import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateUniqueName } from "../utils/shared";
jest.setTimeout(300000);
const RETRY_DELAY = 5000;
const LOADING_STATE_DELAY = 2500;
const RENDER_DELAY = 1000;
jest.setTimeout(120000);
describe("Collection Add and Delete Tables spec", () => {
it("creates a collection", async () => {
try {
const tableId = generateUniqueName("tab");
const frame = await login(process.env.TABLES_CONNECTION_STRING);
test("Tables CRUD", async () => {
const tableId = generateUniqueName("table");
// create new collection
await frame.waitFor('button[data-test="New Table"]', { visible: true });
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click('button[data-test="New Table"]');
// type database id
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]');
await dbInput.press("Backspace");
await dbInput.type(tableId);
// click submit
await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection");
// validate created
// open database menu
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(`div[data-test="TablesDB"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", (elements) => {
return elements.some((el) => el.textContent.includes("Successfully created"));
});
expect(didCreateContainer).toBe(true);
await frame.waitFor(`div[data-test="TablesDB"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await clickTablesMenu(frame);
const collections = await frame.$$(
`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`
);
const textId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, collections[0]);
await frame.waitFor(`div[data-test="${textId}"]`, { visible: true });
// delete container
// click context menu for container
await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true });
await frame.click(`div[data-test="${textId}"] > div > button`);
// click delete container
await frame.waitFor(RENDER_DELAY);
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
// confirm delete container
await frame.waitFor('input[id="confirmCollectionId"]', { visible: true });
await frame.type('input[id="confirmCollectionId"]', textId);
// click delete
await frame.waitFor("button.genericPaneSubmitBtn", { visible: true });
await frame.click("button.genericPaneSubmitBtn");
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await expect(page).not.toMatchElement(`div[data-test="${textId}"]`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `failed-${testName}.jpg` });
throw error;
}
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
await explorer.click('[data-test="New Table"]');
await explorer.click('[data-test="addCollection-collectionId"]');
await explorer.fill('[data-test="addCollection-collectionId"]', tableId);
await explorer.click('[data-test="addCollection-createCollection"]');
await safeClick(explorer, `[data-test="TablesDB"]`);
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
await explorer.click('[aria-label="Submit"]');
await expect(explorer).not.toHaveText(".dataResourceTree", tableId);
});
async function clickTablesMenu(frame: Frame, retries = 0) {
const button = await frame.$(`div[data-test="TablesDB"]`);
await button.focus();
const handler = await button.asElement();
await handler.click();
await ensureMenuIsOpen(frame, retries);
return button;
}
async function ensureMenuIsOpen(frame: Frame, retries: number) {
await frame.waitFor(RETRY_DELAY);
const button = await frame.$(`div[data-test="TablesDB"]`);
const classList = await frame.evaluate((button) => {
return button.parentElement.classList;
}, button);
if (!Object.values(classList).includes("selected") && retries < 5) {
retries = retries + 1;
await clickTablesMenu(frame, retries);
}
}

11
test/utils/safeClick.ts Normal file
View File

@ -0,0 +1,11 @@
import { Frame } from "playwright";
export async function safeClick(page: Frame, selector: string): Promise<void> {
// TODO: Remove. Playwright does this for you... mostly.
// But our knockout+react setup sometimes leaves dom nodes detached and even playwright can't recover.
// Resource tree is particually bad.
// Ideally this should only be added as a last resort
await page.waitForSelector(selector);
await page.waitForTimeout(5000);
await page.click(selector);
}

View File

@ -1,26 +1,4 @@
import crypto from "crypto";
import { Frame } from "puppeteer";
const LOADING_STATE_DELAY = 3000;
const CREATE_DELAY = 10000;
export async function login(connectionString: string): Promise<Frame> {
const prodUrl = process.env.DATA_EXPLORER_ENDPOINT;
await page.goto(prodUrl);
if (process.env.PLATFORM === "Emulator") {
return page.mainFrame();
}
// log in with connection string
await page.waitFor("div > p.switchConnectTypeText", { visible: true });
await page.click("div > p.switchConnectTypeText");
const connStr = connectionString;
await page.type("input[class='inputToken']", connStr);
await page.click("input[value='Connect']");
const handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame();
return frame;
}
export function generateUniqueName(baseName = "", length = 4): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}`;
@ -29,50 +7,3 @@ export function generateUniqueName(baseName = "", length = 4): string {
export function generateDatabaseName(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
}
export async function createDatabase(frame: Frame): Promise<{ databaseId: string; collectionId: string }> {
const databaseId = generateDatabaseName();
const collectionId = generateUniqueName("col");
const shardKey = "partitionKey";
// create new collection
await frame.waitFor('button[data-test="New Collection"]', { visible: true });
await frame.click('button[data-test="New Collection"]');
// check new database
await frame.waitFor('input[data-test="addCollection-createNewDatabase"]');
await frame.click('input[data-test="addCollection-createNewDatabase"]');
// check shared throughput
await frame.waitFor('input[data-test="addCollectionPane-databaseSharedThroughput"]');
await frame.click('input[data-test="addCollectionPane-databaseSharedThroughput"]');
// type database id
await frame.waitFor('input[data-test="addCollection-newDatabaseId"]');
const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]');
await dbInput.press("Backspace");
await dbInput.type(databaseId);
// type collection id
await frame.waitFor('input[data-test="addCollection-collectionId"]');
const input = await frame.$('input[data-test="addCollection-collectionId"]');
await input.press("Backspace");
await input.type(collectionId);
// type partition key value
await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]');
const keyInput = await frame.$('input[data-test="addCollection-partitionKeyValue"]');
await keyInput.press("Backspace");
await keyInput.type(shardKey);
// click submit
await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection");
return { databaseId, collectionId };
}
export async function onClickSaveButton(frame: Frame): Promise<void> {
await frame.waitFor(`button[data-test="Save"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`button[data-test="Save"]`);
await frame.waitFor(CREATE_DELAY);
}

View File

@ -1,22 +0,0 @@
const { AxePuppeteer } = require("axe-puppeteer");
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({ ignoreHTTPSErrors: true });
const page = await browser.newPage();
await page.setBypassCSP(true);
await page.goto("https://localhost:1234/hostedExplorer.html");
const results = await new AxePuppeteer(page).withTags(["wcag2a", "wcag2aa"]).analyze();
if (results.violations && results.violations.length && results.violations.length > 0) {
throw results.violations;
}
await page.close();
await browser.close();
console.log(`Accessibility Check Passed!`);
})().catch(err => {
console.error(`Accessibility Check Failed: ${err.length} Errors`);
console.error(err);
process.exit(1);
});

View File

@ -18,7 +18,6 @@ function friendlyTime(date) {
}
}
// Deletes all SQL and Mongo databases created more than 20 minutes ago in the test runner accounts
async function main() {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const client = new CosmosDBManagementClient(credentials, subscriptionId);
@ -27,6 +26,7 @@ async function main() {
if (account.kind === "MongoDB") {
const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name);
for (const database of mongoDatabases) {
// Unfortunately Mongo does not provide a timestamp in ARM. There is no way to tell how old the DB is other thn encoding it in the ID :(
const timestamp = Number(database.name.split("-")[1]);
if (timestamp && timestamp < thirtyMinutesAgo) {
await client.mongoDBResources.deleteMongoDBDatabase(resourceGroupName, account.name, database.name);
@ -35,10 +35,46 @@ async function main() {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
}
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
resourceGroupName,
account.name
);
for (const database of cassandraDatabases) {
const timestamp = Number(database.resource._ts) * 1000;
if (timestamp && timestamp < thirtyMinutesAgo) {
await client.cassandraResources.deleteCassandraKeyspace(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
}
} else if (account.capabilities.find((c) => c.name === "EnableTable")) {
const tablesDatabase = await client.tableResources.listTables(resourceGroupName, account.name);
for (const database of tablesDatabase) {
const timestamp = Number(database.resource._ts) * 1000;
if (timestamp && timestamp < thirtyMinutesAgo) {
await client.tableResources.deleteTable(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
}
} else if (account.capabilities.find((c) => c.name === "EnableGremlin")) {
const graphDatabases = await client.gremlinResources.listGremlinDatabases(resourceGroupName, account.name);
for (const database of graphDatabases) {
const timestamp = Number(database.resource._ts) * 1000;
if (timestamp && timestamp < thirtyMinutesAgo) {
await client.gremlinResources.deleteGremlinDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
}
} else if (account.kind === "GlobalDocumentDB") {
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
for (const database of sqlDatabases) {
const timestamp = Number(database.name.split("-")[1]);
const timestamp = Number(database.resource._ts) * 1000;
if (timestamp && timestamp < thirtyMinutesAgo) {
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);