diff --git a/.eslintrc.js b/.eslintrc.js index cb7e34f01..edfdaddac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { plugins: ["react"] }, { - files: ["**/*.test.{ts,tsx}"], + files: ["**/*.{test,spec}.{ts,tsx}"], env: { jest: true }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3af3790f7..24ef1258d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,28 @@ jobs: shell: bash env: NODE_TLS_REJECT_UNAUTHORIZED: 0 + endtoendpuppeteer: + name: "End to end puppeteer tests" + needs: [lint, format, compile, unittest] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 12.x + uses: actions/setup-node@v1 + with: + node-version: 12.x + - name: End to End Puppeteer Tests + run: | + npm ci + npm start & + npm run wait-for-server + npm run test:e2e + shell: bash + env: + NODE_TLS_REJECT_UNAUTHORIZED: 0 + PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }} + MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }} + CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }} nuget: name: Publish Nuget if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index d78913864..742439a48 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -3,7 +3,7 @@ const isCI = require("is-ci"); module.exports = { launch: { headless: isCI, - slowMo: 50, + slowMo: 30, defaultViewport: null, ignoreHTTPSErrors: true, args: ["--disable-web-security"] diff --git a/package.json b/package.json index c0c72280d..d44467120 100644 --- a/package.json +++ b/package.json @@ -193,8 +193,8 @@ "compile": "tsc", "compile:contracts": "tsc -p ./tsconfig.contracts.json", "compile:strict": "tsc -p ./tsconfig.strict.json", - "format": "prettier --write \"{src,cypress}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", - "format:check": "prettier --check \"{src,cypress}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", + "format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", + "format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "build:contracts": "npm run compile:contracts", "strictEligibleFiles": "node ./strict-migration-tools/index.js", diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 1676fb7d5..1adc01f21 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,49 +1,39 @@ import "expect-puppeteer"; -import crypto from 'crypto' +import { generateUniqueName, login } from "../utils/shared"; jest.setTimeout(300000); -const RENDER_DELAY = 400 -const LOADING_STATE_DELAY = 1800 +const RENDER_DELAY = 400; +const LOADING_STATE_DELAY = 2500; -describe('Collection Add and Delete Cassandra spec', () => { - it('creates a collection', async () => { +describe("Collection Add and Delete Cassandra spec", () => { + it("creates a collection", async () => { try { - const keyspaceId = `keyspaceid${crypto.randomBytes(8).toString("hex")}`; - const tableId = `tableid${crypto.randomBytes(3).toString('hex')}`; - const prodUrl = "https://localhost:1234/hostedExplorer.html"; - page.goto(prodUrl); - - // log in with connection string - const handle = await page.waitForSelector('iframe'); - const frame = await handle.contentFrame(); - await frame.waitFor('div > p.switchConnectTypeText', { visible: true }); - await frame.click('div > p.switchConnectTypeText'); - const connStr = process.env.CASSANDRA_CONNECTION_STRING; - await frame.type("input[class='inputToken']", connStr); - await frame.click("input[value='Connect']"); + const keyspaceId = generateUniqueName("keyspaceid"); + const tableId = generateUniqueName("tableid"); + 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"]'); + 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); + await frame.type('input[id="keyspace-id"]', keyspaceId); // type table id await frame.waitFor('input[class="textfontclr"]'); - await frame.type('input[class="textfontclr"]', tableId); + 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'); + 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(`div[data-test="${keyspaceId}"]`, { visible: true }); - await frame.waitFor(LOADING_STATE_DELAY) + await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(`div[data-test="${keyspaceId}"]`, { visible: true }); await frame.click(`div[data-test="${keyspaceId}"]`); await frame.waitFor(`span[title="${tableId}"]`, { visible: true }); @@ -55,16 +45,17 @@ describe('Collection Add and Delete Cassandra spec', () => { await frame.click(`div[data-test="${tableId}"] > div > button`); // click delete container - await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed'); - await frame.waitFor(RENDER_DELAY) - const elements = await frame.$$('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]') - await elements[0].click() + await frame.waitForSelector("body > div.ms-Layer.ms-Layer--fixed"); + await frame.waitFor(RENDER_DELAY); + const elements = await frame.$$('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + await elements[0].click(); // confirm delete container await frame.type('input[data-test="confirmCollectionId"]', tableId.trim()); // click delete await frame.click('input[data-test="deleteCollection"]'); + 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 }); @@ -78,10 +69,12 @@ describe('Collection Add and Delete Cassandra spec', () => { // click delete database await frame.waitFor(RENDER_DELAY); - const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]') + const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]'); await dbElements[0].click(); // confirm delete database + await frame.waitForSelector('input[data-test="confirmDatabaseId"]', { visible: true }); + await frame.waitFor(RENDER_DELAY); await frame.type('input[data-test="confirmDatabaseId"]', keyspaceId.trim()); // click delete @@ -90,9 +83,9 @@ describe('Collection Add and Delete Cassandra spec', () => { 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: `Test Failed ${testName}.png`}); + const testName = (expect as any).getState().currentTestName; + await page.screenshot({ path: `Test Failed ${testName}.png` }); throw error; - } + } }); -}); \ No newline at end of file +}); diff --git a/test/index.spec.ts b/test/index.spec.ts index 51152a775..6c227768e 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -50,7 +50,7 @@ describe.skip("Collection CRUD", () => { // .find('div[class="treeComponent dataResourceTree"]') // .should("contain", dbId); } catch (error) { - await page.screenshot({path: 'failure.png'}); + await page.screenshot({ path: "failure.png" }); trackException(error); throw error; } diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts new file mode 100644 index 000000000..03810d6c2 --- /dev/null +++ b/test/mongo/container.spec.ts @@ -0,0 +1,105 @@ +import "expect-puppeteer"; +import { generateUniqueName, login } from "../utils/shared"; + +jest.setTimeout(300000); + +const LOADING_STATE_DELAY = 2500; +const RENDER_DELAY = 1000; + +describe("Collection Add and Delete Mongo spec", () => { + it("creates and deletes a collection", async () => { + try { + const dbId = generateUniqueName("TestDatabase"); + const collectionId = generateUniqueName("TestCollection"); + const sharedKey = generateUniqueName("SharedKey"); + 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"]'); + await frame.type('input[data-test="addCollection-newDatabaseId"]', dbId); + + // type collection id + await frame.waitFor('input[data-test="addCollection-collectionId"]'); + await frame.type('input[data-test="addCollection-collectionId"]', collectionId); + + // type partition key value + await frame.waitFor('input[data-test="addCollection-partitionKeyValue"]'); + await frame.type('input[data-test="addCollection-partitionKeyValue"]', sharedKey); + + // click submit + await frame.waitFor("#submitBtnAddCollection"); + await frame.click("#submitBtnAddCollection"); + + // validate created + // open database menu + await frame.waitFor(`span[title="${dbId}"]`); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + + await frame.waitFor(`div[data-test="${dbId}"]`), { visible: true }; + await frame.click(`div[data-test="${dbId}"]`); + await frame.waitFor(RENDER_DELAY); + await frame.waitFor(`div[data-test="${collectionId}"]`, { visible: true }); + + // delete container + + // click context menu for container + await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true }); + await frame.click(`div[data-test="${collectionId}"] > 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[data-test="confirmCollectionId"]', { visible: true }); + await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim()); + + // click delete + await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); + await frame.click('input[data-test="deleteCollection"]'); + 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="${collectionId}"]`); + + // click context menu for database + await frame.waitFor(`div[data-test="${dbId}"] > div > button`); + const button = await frame.$(`div[data-test="${dbId}"] > div > button`); + await button.focus(); + await button.asElement().click(); + + // click delete database + await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]'); + await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]'); + + // confirm delete database + await frame.waitForSelector('input[data-test="confirmDatabaseId"]', { visible: true }); + await frame.waitFor(RENDER_DELAY); + await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim()); + + // click delete + await frame.click('input[data-test="deleteDatabase"]'); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`); + } 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}.png` }); + throw error; + } + }); +}); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index c77216e35..3d1e10fdf 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,25 +1,17 @@ import "expect-puppeteer"; -import crypto from "crypto"; +import { generateUniqueName, login } from "../utils/shared"; jest.setTimeout(300000); +const LOADING_STATE_DELAY = 2500; +const RENDER_DELAY = 1000; describe("Collection Add and Delete SQL spec", () => { it("creates a collection", async () => { try { - const dbId = `TestDatabase${crypto.randomBytes(8).toString("hex")}`; - const collectionId = `TestCollection${crypto.randomBytes(8).toString("hex")}`; - const sharedKey = `SharedKey${crypto.randomBytes(8).toString("hex")}`; - const prodUrl = "https://localhost:1234/hostedExplorer.html"; - page.goto(prodUrl); - - // log in with connection string - const handle = await page.waitForSelector("iframe"); - const frame = await handle.contentFrame(); - await frame.waitFor("div > p.switchConnectTypeText", { visible: true }); - await frame.click("div > p.switchConnectTypeText"); - const connStr = process.env.PORTAL_RUNNER_CONNECTION_STRING; - await frame.type("input[class='inputToken']", connStr); - await frame.click("input[value='Connect']"); + const dbId = generateUniqueName("TestDatabase"); + const collectionId = generateUniqueName("TestCollection"); + const sharedKey = generateUniqueName("SharedKey"); + const frame = await login(process.env.PORTAL_RUNNER_CONNECTION_STRING); // create new collection await frame.waitFor('button[data-test="New Container"]', { visible: true }); @@ -55,20 +47,20 @@ describe("Collection Add and Delete SQL spec", () => { await frame.waitFor(`span[title="${dbId}"]`); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.waitFor(`div[data-test="${dbId}"]`), { visible: true }; await frame.click(`div[data-test="${dbId}"]`); - await frame.waitFor(3000); - await frame.waitFor(`span[title="${collectionId}"]`, { visible: true }); + await frame.waitFor(RENDER_DELAY); + await frame.waitFor(`div[data-test="${collectionId}"]`, { visible: true }); // delete container // click context menu for container await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true }); - await frame.waitFor(`span[title="${collectionId}"]`, { visible: true }); await frame.click(`div[data-test="${collectionId}"] > div > button`); - await frame.waitFor(2000); // click delete container - await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true }); + await frame.waitFor(RENDER_DELAY); + await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); // confirm delete container @@ -78,7 +70,7 @@ describe("Collection Add and Delete SQL spec", () => { // click delete await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); await frame.click('input[data-test="deleteCollection"]'); - await frame.waitFor(5000); + 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="${collectionId}"]`); @@ -94,6 +86,8 @@ describe("Collection Add and Delete SQL spec", () => { await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]'); // confirm delete database + await frame.waitForSelector('input[data-test="confirmDatabaseId"]', { visible: true }); + await frame.waitFor(RENDER_DELAY); await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim()); // click delete diff --git a/test/utils/shared.ts b/test/utils/shared.ts new file mode 100644 index 000000000..458899700 --- /dev/null +++ b/test/utils/shared.ts @@ -0,0 +1,21 @@ +import crypto from "crypto"; +import { Frame } from "puppeteer"; + +export async function login(connectionString: string): Promise { + const prodUrl = "https://localhost:1234/hostedExplorer.html"; + page.goto(prodUrl); + + // log in with connection string + const handle = await page.waitForSelector("iframe"); + const frame = await handle.contentFrame(); + await frame.waitFor("div > p.switchConnectTypeText", { visible: true }); + await frame.click("div > p.switchConnectTypeText"); + const connStr = connectionString; + await frame.type("input[class='inputToken']", connStr); + await frame.click("input[value='Connect']"); + return frame; +} + +export function generateUniqueName(baseName: string, length = 8): string { + return `${baseName}${crypto.randomBytes(length).toString("hex")}`; +}