From ab28dab75b78a0aa898da0ee6e01eb2726aba720 Mon Sep 17 00:00:00 2001 From: Lake Mossman Date: Wed, 31 Jan 2024 19:29:07 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20connector=20builder?= =?UTF-8?q?=20E2E=20tests=20to=20be=20less=20flaky=20and=20more=20retryabl?= =?UTF-8?q?e=20(#11015)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alex Birdsall --- airbyte-webapp/cypress/commands/connection.ts | 4 -- .../cypress/commands/connectorBuilder.ts | 35 ++++++------- .../cypress/e2e/connectorBuilder.cy.ts | 27 +++------- .../cypress/pages/connectorBuilderPage.ts | 49 +++++++------------ 4 files changed, 41 insertions(+), 74 deletions(-) diff --git a/airbyte-webapp/cypress/commands/connection.ts b/airbyte-webapp/cypress/commands/connection.ts index c5127ef3ea0..04b7d6591ea 100644 --- a/airbyte-webapp/cypress/commands/connection.ts +++ b/airbyte-webapp/cypress/commands/connection.ts @@ -62,10 +62,6 @@ export const createTestConnection = (sourceName: string, destinationName: string createLocalJsonDestination(destinationName); } - // TODO is this actually needed? - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(5000); - cy.get("a[data-testid='connections-step']").click(); openCreateConnection(); diff --git a/airbyte-webapp/cypress/commands/connectorBuilder.ts b/airbyte-webapp/cypress/commands/connectorBuilder.ts index d30e92a8926..29bed847934 100644 --- a/airbyte-webapp/cypress/commands/connectorBuilder.ts +++ b/airbyte-webapp/cypress/commands/connectorBuilder.ts @@ -3,9 +3,6 @@ import { assertHasNumberOfPages, configureLimitOffsetPagination, configureParameters, - disableAutoImportSchema, - disablePagination, - disableStreamSlicer, enablePagination, enableParameterizedRequests, enterName, @@ -38,6 +35,8 @@ export const configureGlobals = (name: string) => { } else { enterUrlBase("http://172.17.0.1:6767/"); } + + configureAuth(); }; export const configureStream = () => { @@ -46,7 +45,6 @@ export const configureStream = () => { enterUrlPathFromForm("items/"); submitForm(); enterRecordSelector("items"); - disableAutoImportSchema(); }; export const configureAuth = () => { @@ -55,7 +53,6 @@ export const configureAuth = () => { openTestInputs(); enterTestInputs({ apiKey: "theauthkey" }); submitForm(); - goToView("0"); }; export const configurePagination = () => { @@ -71,18 +68,11 @@ export const configureParameterizedRequests = (numberOfParameters: number) => { enterUrlPath("items/{{}{{} stream_slice.item_id }}"); }; -export const cleanUp = () => { - goToView("0"); - cy.get('[data-testid="tag-tab-stream-configuration"]').click({ force: true }); - disablePagination(); - disableStreamSlicer(); -}; - export const publishProject = () => { // debounce is 2500 so we need to wait at least more before change page // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(30000); - cy.get('[data-testid="publish-button"]').click({ force: true }); + cy.wait(3000); + cy.get('[data-testid="publish-button"]').click(); submitForm(); }; @@ -159,11 +149,18 @@ const SCHEMA_WITH_MISMATCH = '{{}"$schema": "http://json-schema.org/schema#", "properties": {{}"name": {{}"type": "number"}}, "type": "object"}'; export const acceptSchemaWithMismatch = () => { openStreamSchemaTab(); - cy.get("textarea").clear({ force: true }); - // TODO is this actually needed? - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(500); - cy.get("textarea").type(SCHEMA_WITH_MISMATCH, { force: true }); + // When running against local dev webapp, some uncaught exceptions may be thrown when the monaco editor is loaded. + // Ignore them since they do not affect the test. + cy.on("uncaught:exception", (err) => { + const monacoLoadCancelled = (err as { msg?: string })?.msg?.includes("operation is manually canceled"); + const monacoScriptLoadFailed = err?.message?.includes("importScripts") && err?.message?.includes("monaco-editor"); + if (monacoLoadCancelled || monacoScriptLoadFailed) { + return false; + } + }); + cy.get("label").contains("Automatically import detected schema").click(); + cy.get("textarea").clear(); + cy.get("textarea").type(SCHEMA_WITH_MISMATCH); }; export const assertSchemaMismatch = () => { diff --git a/airbyte-webapp/cypress/e2e/connectorBuilder.cy.ts b/airbyte-webapp/cypress/e2e/connectorBuilder.cy.ts index b2c272cc3e3..6cc351122e9 100644 --- a/airbyte-webapp/cypress/e2e/connectorBuilder.cy.ts +++ b/airbyte-webapp/cypress/e2e/connectorBuilder.cy.ts @@ -13,8 +13,6 @@ import { assertTestReadAuthFailure, assertTestReadItems, assertUrlPath, - cleanUp, - configureAuth, configureGlobals, configurePagination, configureParameterizedRequests, @@ -30,14 +28,16 @@ import { goToConnectorBuilderProjectsPage, goToView, selectActiveVersion, + selectAuthMethod, startFromScratch, testStream, } from "pages/connectorBuilderPage"; import { goToSourcePage, openSourceConnectionsPage } from "pages/sourcePage"; describe("Connector builder", { testIsolation: false }, () => { - const connectorName = appendRandomString("dummy_api"); - before(() => { + let connectorName = ""; + beforeEach(() => { + connectorName = appendRandomString("dummy_api"); // Updated for cypress 12 because connector builder uses local storage // docs.cypress.io/guides/references/migration-guide#Simulating-Pre-Test-Isolation-Behavior cy.clearLocalStorage(); @@ -49,30 +49,19 @@ describe("Connector builder", { testIsolation: false }, () => { configureStream(); }); - afterEach(() => { - cleanUp(); - }); - - /* - This test assumes it runs before "Read - Without pagination or parameterized requests" since auth will be configured at that - point - */ it("Fail on invalid auth", () => { cy.on("uncaught:exception", () => false); + goToView("global"); + selectAuthMethod("No Auth"); testStream(); assertTestReadAuthFailure(); }); it("Read - Without pagination or parameterized requests", () => { - configureAuth(); testStream(); assertTestReadItems(); }); - /* - All the tests below assume they run after "Read - Without pagination or parameterized requests" in order to have auth - configured - */ it("Read - Infer schema", () => { testStream(); assertSchema(); @@ -129,7 +118,6 @@ describe("Connector builder", { testIsolation: false }, () => { assertMaxNumberOfSlicesAndPages(); }); - // Note: This test cannot be run in isolation! It is dependent on the previous test it("Sync published version", () => { publishProject(); @@ -162,8 +150,9 @@ describe("Connector builder", { testIsolation: false }, () => { editProjectBuilder(connectorName); }); - // This test assumes the test before is configuring path items/ it("Validate going back to a previously created connector", () => { + configureParameterizedRequests(10); + publishProject(); goToConnectorBuilderProjectsPage(); editProjectBuilder(connectorName); goToView("0"); diff --git a/airbyte-webapp/cypress/pages/connectorBuilderPage.ts b/airbyte-webapp/cypress/pages/connectorBuilderPage.ts index 9533736cfd9..2a190ed6c2e 100644 --- a/airbyte-webapp/cypress/pages/connectorBuilderPage.ts +++ b/airbyte-webapp/cypress/pages/connectorBuilderPage.ts @@ -34,25 +34,25 @@ export const editProjectBuilder = (name: string) => { }; export const startFromScratch = () => { - cy.get(startFromScratchButton).click({ force: true }); + cy.get(startFromScratchButton).click(); }; export const enterName = (name: string) => { cy.get(nameInput).clear(); - cy.get(nameInput).type(name, { force: true }); + cy.get(nameInput).type(name); }; export const enterUrlBase = (urlBase: string) => { - cy.get(urlBaseInput).type(urlBase, { force: true }); + cy.get(urlBaseInput).type(urlBase); }; export const enterRecordSelector = (recordSelector: string) => { - cy.get(recordSelectorInput).first().type(recordSelector, { force: true }); - cy.get(recordSelectorInput).first().type("{enter}", { force: true }); + cy.get(recordSelectorInput).first().type(recordSelector); + cy.get(recordSelectorInput).first().type("{enter}"); }; -const selectFromDropdown = (selector: string, value: string) => { - cy.get(`${selector} .react-select__dropdown-indicator`).last().click({ force: true }); +export const selectFromDropdown = (selector: string, value: string) => { + cy.get(`${selector} .react-select__dropdown-indicator`).last().click(); cy.get(`.react-select__option`).contains(value).click(); }; @@ -75,7 +75,7 @@ export const openTestInputs = () => { }; export const enterTestInputs = ({ apiKey }: { apiKey: string }) => { - cy.get(apiKeyInput).type(apiKey, { force: true }); + cy.get(apiKeyInput).type(apiKey); }; export const goToTestPage = (page: number) => { @@ -83,13 +83,10 @@ export const goToTestPage = (page: number) => { }; export const enablePagination = () => { + // force: true is needed because the input has display: none, as we don't want to show default checkboxes cy.get(togglePaginationInput).check({ force: true }); }; -export const disablePagination = () => { - cy.get(togglePaginationInput).uncheck({ force: true }); -}; - export const configureLimitOffsetPagination = ( limit: string, limitInto: string, @@ -97,29 +94,23 @@ export const configureLimitOffsetPagination = ( offsetInto: string, offsetFieldName: string ) => { - cy.get(limitInput).type(limit, { force: true }); + cy.get(limitInput).type(limit); selectFromDropdown(injectLimitInto, limitInto); cy.get(injectLimitFieldName).type(limitFieldName); selectFromDropdown(injectOffsetInto, offsetInto); - cy.get(injectOffsetFieldName).type(offsetFieldName, { force: true }); + cy.get(injectOffsetFieldName).type(offsetFieldName); }; export const enableParameterizedRequests = () => { + // force: true is needed because the input has display: none, as we don't want to show default checkboxes cy.get(toggleParameterizedRequestsInput).check({ force: true }); }; -export const disableStreamSlicer = () => { - cy.get(toggleParameterizedRequestsInput).uncheck({ force: true }); -}; - export const configureParameters = (values: string, cursor_field: string) => { cy.get('[data-testid="tag-input-formValues.streams.0.parameterizedRequests.0.values.value"] input[type="text"]').type( - values, - { - force: true, - } + values ); - cy.get("[name='formValues.streams.0.parameterizedRequests.0.cursor_field']").type(cursor_field, { force: true }); + cy.get("[name='formValues.streams.0.parameterizedRequests.0.cursor_field']").type(cursor_field); }; export const getSlicesFromDropdown = () => { @@ -143,22 +134,16 @@ export const getDetectedSchemaElement = () => { return cy.get('pre[class*="SchemaDiffView"]'); }; -export const disableAutoImportSchema = () => { - openStreamSchemaTab(); - cy.get("label").contains("Automatically import detected schema").click(); - openStreamConfigurationTab(); -}; - export const addStream = () => { cy.get(addStreamButton).click(); }; export const enterStreamName = (streamName: string) => { - cy.get(streamNameInput).type(streamName, { force: true }); + cy.get(streamNameInput).type(streamName); }; export const enterUrlPathFromForm = (urlPath: string) => { - cy.get(streamUrlPathFromModal).type(urlPath, { force: true }); + cy.get(streamUrlPathFromModal).type(urlPath); }; export const getUrlPathInput = () => { @@ -168,7 +153,7 @@ export const getUrlPathInput = () => { export const enterUrlPath = (urlPath: string) => { cy.get('[name="formValues.streams.0.urlPath"]').focus(); cy.get('[name="formValues.streams.0.urlPath"]').clear(); - cy.get('[name="formValues.streams.0.urlPath"]').type(urlPath, { force: true }); + cy.get('[name="formValues.streams.0.urlPath"]').type(urlPath); }; export const submitForm = () => {