diff --git a/airbyte-webapp-e2e-tests/cypress/commands/common.ts b/airbyte-webapp-e2e-tests/cypress/commands/common.ts index 030e4e924f5f..80c78caa697b 100644 --- a/airbyte-webapp-e2e-tests/cypress/commands/common.ts +++ b/airbyte-webapp-e2e-tests/cypress/commands/common.ts @@ -31,3 +31,21 @@ export const appendRandomString = (string: string) => { const randomString = Math.random().toString(36).substring(2, 10); return `${string} _${randomString}`; }; + +/** + * Click on specific cell found by column name in desired table + * @param tableSelector - table selector + * @param columnName - column name + * @param connectName - cell text + */ +export const clickOnCellInTable = (tableSelector: string, columnName: string, connectName: string) => { + cy.contains(`${tableSelector} th`, columnName) + .invoke("index") + .then((value) => { + cy.log(`${value}`); + return cy.wrap(value); + }) + .then((columnIndex) => { + cy.contains("tbody tr", connectName).find("td").eq(columnIndex).click(); + }); +}; diff --git a/airbyte-webapp-e2e-tests/cypress/commands/interceptors.ts b/airbyte-webapp-e2e-tests/cypress/commands/interceptors.ts new file mode 100644 index 000000000000..9a35ba0ce386 --- /dev/null +++ b/airbyte-webapp-e2e-tests/cypress/commands/interceptors.ts @@ -0,0 +1,6 @@ +export const interceptGetConnectionRequest = () => cy.intercept("/api/v1/web_backend/connections/get").as("getConnection") +export const waitForGetConnectionRequest = () => cy.wait("@getConnection"); + +export const interceptUpdateConnectionRequest = () => + cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); +export const waitForUpdateConnectionRequest = () => cy.wait("@updateConnection", { timeout: 10000 }); diff --git a/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts b/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts index cb72a6031f97..160af7c0d751 100644 --- a/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts +++ b/airbyte-webapp-e2e-tests/cypress/integration/connection.spec.ts @@ -23,11 +23,11 @@ import { selectPrimaryKeyField, checkPreFilledPrimaryKeyField, checkStreamFields, - expandStreamDetails, + expandStreamDetailsByName, } from "pages/replicationPage"; -import { openSourceDestinationFromGrid, goToSourcePage } from "pages/sourcePage"; -import { goToSettingsPage } from "pages/settingsConnectionPage"; -import { cleanDBSource, makeChangesInDBSource, populateDBSource } from "../commands/db"; +import { goToSourcePage, openSourceOverview } from "pages/sourcePage"; +import { goToSettingsPage, openConnectionOverviewByDestinationName } from "pages/settingsConnectionPage"; +import { cleanDBSource, makeChangesInDBSource, populateDBSource } from "commands/db"; import { catalogDiffModal, newFieldsTable, @@ -35,12 +35,21 @@ import { removedFieldsTable, removedStreamsTable, toggleStreamWithChangesAccordion, -} from "../pages/modals/catalogDiffModal"; -import { updateSchemaModalConfirmBtnClick } from "../pages/modals/updateSchemaModal"; +} from "pages/modals/catalogDiffModal"; +import { updateSchemaModalConfirmBtnClick } from "pages/modals/updateSchemaModal"; +import { + interceptGetConnectionRequest, + interceptUpdateConnectionRequest, + waitForGetConnectionRequest, + waitForUpdateConnectionRequest, +} from "commands/interceptors"; describe("Connection - creation, updating connection replication settings, deletion", () => { beforeEach(() => { initialSetupCompleted(); + + interceptGetConnectionRequest(); + interceptUpdateConnectionRequest(); }); it("Create Postgres <> LocalJSON connection, check it's creation", () => { @@ -56,16 +65,14 @@ describe("Connection - creation, updating connection replication settings, delet }); it("Create Postgres <> LocalJSON connection, update connection replication settings - select schedule and add destination prefix", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - const sourceName = appendRandomString("Test update connection source cypress"); const destName = appendRandomString("Test update connection destination cypress"); createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); @@ -74,7 +81,7 @@ describe("Connection - creation, updating connection replication settings, delet submitButtonClick(); - cy.wait("@updateConnection").then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); }); @@ -86,16 +93,14 @@ describe("Connection - creation, updating connection replication settings, delet it(`Creates PokeAPI <> Local JSON connection, update connection replication settings - select schedule, add destination prefix, set destination namespace custom format, change prefix and make sure that it's applied to all streams`, () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - const sourceName = appendRandomString("Test update connection PokeAPI source cypress"); const destName = appendRandomString("Test update connection Local JSON destination cypress"); createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); @@ -113,7 +118,7 @@ describe("Connection - creation, updating connection replication settings, delet submitButtonClick(); confirmStreamConfigurationChangedPopup(); - cy.wait("@updateConnection").then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); expect(interception.request.method).to.eq("POST"); expect(interception.request) @@ -174,20 +179,17 @@ describe("Connection - creation, updating connection replication settings, delet }); it("Create PokeAPI <> Local JSON connection, update connection replication settings - make sure that saving a connection's schedule type only changes expected values", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - cy.intercept("/api/v1/web_backend/connections/get").as("getConnection"); - const sourceName = appendRandomString("Test update connection PokeAPI source cypress"); const destName = appendRandomString("Test update connection Local JSON destination cypress"); createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(`${sourceName} <> ${destName}`); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); let loadedConnection: any = null; // Should be a WebBackendConnectionRead - cy.wait("@getConnection").then((interception) => { + waitForGetConnectionRequest().then((interception) => { const { scheduleType: readScheduleType, scheduleData: readScheduleData, @@ -205,7 +207,7 @@ describe("Connection - creation, updating connection replication settings, delet selectSchedule("Every hour"); submitButtonClick(); - cy.wait("@updateConnection").then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { // Schedule is pulled out here, but we don't do anything with is as it's legacy const { scheduleType, scheduleData, schedule, ...connectionUpdate } = interception.response?.body; expect(scheduleType).to.eq("basic"); @@ -228,8 +230,8 @@ describe("Connection - creation, updating connection replication settings, delet createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToSettingsPage(); @@ -240,16 +242,14 @@ describe("Connection - creation, updating connection replication settings, delet }); it("Create PokeAPI <> Local JSON connection, update connection replication settings - set destination namespace with 'Custom format' option", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - const sourceName = appendRandomString("Test update connection PokeAPI source cypress"); const destName = appendRandomString("Test update connection Local JSON destination cypress"); createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); @@ -261,7 +261,7 @@ describe("Connection - creation, updating connection replication settings, delet submitButtonClick(); - cy.wait("@updateConnection").then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); expect(interception.request.method).to.eq("POST"); expect(interception.request) @@ -286,16 +286,14 @@ describe("Connection - creation, updating connection replication settings, delet }); it("Create PokeAPI <> Local JSON connection, update connection replication settings - set destination namespace with 'Mirror source structure' option", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - const sourceName = appendRandomString("Test update connection PokeAPI source cypress"); const destName = appendRandomString("Test update connection Local JSON destination cypress"); createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); @@ -309,16 +307,14 @@ describe("Connection - creation, updating connection replication settings, delet }); it("Create PokeAPI <> Local JSON connection, update connection replication settings - set destination namespace with 'Destination default' option", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - const sourceName = appendRandomString("Test update connection PokeAPI source cypress"); const destName = appendRandomString("Test update connection Local JSON destination cypress"); createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); @@ -331,7 +327,7 @@ describe("Connection - creation, updating connection replication settings, delet submitButtonClick(); - cy.wait("@updateConnection").then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); expect(interception.request.method).to.eq("POST"); expect(interception.request) @@ -369,20 +365,21 @@ describe("Connection - stream details", () => { it("Create Postgres <> Postgres connection, connection replication settings, expand stream details", () => { const sourceName = appendRandomString("Test connection Postgres source cypress"); const destName = appendRandomString("Test connection Postgres destination cypress"); + const streamName = "users"; - const collectionNames = ["Field name", "col1", "id"]; - const collectionTypes = ["Data type", "String", "Integer"]; + const collectionNames = ["col1", "id"]; + const collectionTypes = ["String", "Integer"]; createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); - searchStream("users"); - expandStreamDetails(); + searchStream(streamName); + expandStreamDetailsByName(streamName); checkStreamFields(collectionNames, collectionTypes); deleteSource(sourceName); @@ -394,6 +391,8 @@ describe("Connection sync modes", () => { beforeEach(() => { initialSetupCompleted(); populateDBSource(); + + interceptUpdateConnectionRequest(); }); afterEach(() => { @@ -403,39 +402,38 @@ describe("Connection sync modes", () => { it("Create Postgres <> Postgres connection, update connection replication settings - select 'Incremental Append' sync mode, select required Cursor field, verify changes", () => { const sourceName = appendRandomString("Test connection Postgres source cypress"); const destName = appendRandomString("Test connection Postgres destination cypress"); - - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); + const streamName = "users"; createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); - searchStream("users"); + searchStream(streamName); selectSyncMode("Incremental", "Append"); - selectCursorField("col1"); + selectCursorField(streamName, "col1"); submitButtonClick(); confirmStreamConfigurationChangedPopup(); - cy.wait("@updateConnection", { timeout: 5000 }).then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); }); checkSuccessResult(); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); searchStream("users"); //FIXME: rename "check" to "verify" or similar - checkCursorField("col1"); + checkCursorField(streamName, "col1"); deleteSource(sourceName); deleteDestination(destName); @@ -444,41 +442,40 @@ describe("Connection sync modes", () => { it("Create Postgres <> Postgres connection, update connection replication settings - select 'Incremental Deduped History'(PK is defined), select Cursor field, verify changes", () => { const sourceName = appendRandomString("Test connection Postgres source cypress"); const destName = appendRandomString("Test connection Postgres destination cypress"); - - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); + const streamName = "users"; createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); - searchStream("users"); + searchStream(streamName); selectSyncMode("Incremental", "Deduped + history"); - selectCursorField("col1"); - checkPreFilledPrimaryKeyField("id"); + selectCursorField(streamName, "col1"); + checkPreFilledPrimaryKeyField(streamName, "id"); submitButtonClick(); confirmStreamConfigurationChangedPopup(); - cy.wait("@updateConnection", { timeout: 5000 }).then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); }); checkSuccessResult(); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); - searchStream("users"); + searchStream(streamName); - checkCursorField("col1"); - checkPreFilledPrimaryKeyField("id"); + checkCursorField(streamName, "col1"); + checkPreFilledPrimaryKeyField(streamName, "id"); deleteSource(sourceName); deleteDestination(destName); @@ -487,42 +484,41 @@ describe("Connection sync modes", () => { it("Create Postgres <> Postgres connection, update connection replication settings - select 'Incremental Deduped History'(PK is NOT defined), select Cursor field, select PK, verify changes", () => { const sourceName = appendRandomString("Test connection Postgres source cypress"); const destName = appendRandomString("Test connection Postgres destination cypress"); - - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); + const streamName = "cities"; createTestConnection(sourceName, destName); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); - searchStream("cities"); + searchStream(streamName); selectSyncMode("Incremental", "Deduped + history"); - selectCursorField("city"); - isPrimaryKeyNonExist(); - selectPrimaryKeyField("city_code"); + selectCursorField(streamName, "city"); + isPrimaryKeyNonExist(streamName); + selectPrimaryKeyField(streamName, ["city_code"]); submitButtonClick(); confirmStreamConfigurationChangedPopup(); - cy.wait("@updateConnection", { timeout: 5000 }).then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); }); checkSuccessResult(); goToSourcePage(); - openSourceDestinationFromGrid(sourceName); - openSourceDestinationFromGrid(destName); + openSourceOverview(sourceName); + openConnectionOverviewByDestinationName(destName); goToReplicationTab(); - searchStream("cities"); + searchStream(streamName); - checkCursorField("city"); - checkPrimaryKey("city_code"); + checkCursorField(streamName, "city"); + checkPrimaryKey(streamName, ["city_code"]); deleteSource(sourceName); deleteDestination(destName); @@ -533,6 +529,8 @@ describe("Connection - detect source schema changes in source", () => { beforeEach(() => { initialSetupCompleted(); populateDBSource(); + + interceptUpdateConnectionRequest(); }); afterEach(() => { @@ -540,8 +538,6 @@ describe("Connection - detect source schema changes in source", () => { }); it("Create Postgres <> Local JSON connection, update data in source (async), refresh source schema, check diff modal, reset streams", () => { - cy.intercept("/api/v1/web_backend/connections/update").as("updateConnection"); - const sourceName = appendRandomString( "Test refresh source schema with changed data - connection Postgres source cypress" ); @@ -574,7 +570,7 @@ describe("Connection - detect source schema changes in source", () => { submitButtonClick(); resetModalSaveBtnClick(); - cy.wait("@updateConnection").then((interception) => { + waitForUpdateConnectionRequest().then((interception) => { assert.isNotNull(interception.response?.statusCode, "200"); }); diff --git a/airbyte-webapp-e2e-tests/cypress/pages/createConnectorPage.ts b/airbyte-webapp-e2e-tests/cypress/pages/createConnectorPage.ts index f2ae7a529c4c..44bee2632352 100644 --- a/airbyte-webapp-e2e-tests/cypress/pages/createConnectorPage.ts +++ b/airbyte-webapp-e2e-tests/cypress/pages/createConnectorPage.ts @@ -1,4 +1,5 @@ const selectTypeDropdown = "div[data-testid='serviceType']"; +const getServiceTypeDropdownOption = (serviceName: string) => `div[data-testid='${serviceName}']`; const nameInput = "input[name=name]"; const hostInput = "input[name='connectionConfiguration.host']"; const portInput = "input[name='connectionConfiguration.port']"; @@ -9,10 +10,11 @@ const pokemonNameInput = "input[name='connectionConfiguration.pokemon_name']"; const schemaInput = "[data-testid='tag-input'] input"; const destinationPathInput = "input[name='connectionConfiguration.destination_path']"; -export const selectServiceType = (type: string) => { - cy.get(selectTypeDropdown).click(); - cy.get("div").contains(type).click(); -}; +export const selectServiceType = (type: string) => + cy + .get(selectTypeDropdown) + .click() + .within(() => cy.get(getServiceTypeDropdownOption(type)).click()); export const enterName = (name: string) => { cy.get(nameInput).clear().type(name); diff --git a/airbyte-webapp-e2e-tests/cypress/pages/replicationPage.ts b/airbyte-webapp-e2e-tests/cypress/pages/replicationPage.ts index 8cfb7c0d32a8..e833fc14be34 100644 --- a/airbyte-webapp-e2e-tests/cypress/pages/replicationPage.ts +++ b/airbyte-webapp-e2e-tests/cypress/pages/replicationPage.ts @@ -8,20 +8,22 @@ const destinationNamespaceDefault = "div[data-testid='namespaceDefinition-destin const destinationNamespaceSource = "div[data-testid='namespaceDefinition-source']"; const destinationNamespaceCustomInput = "input[data-testid='input']"; const syncModeDropdown = "div[data-testid='syncSettingsDropdown'] input"; -const cursorFieldDropdown = "button[class^='PathPopoutButton_button']"; -const streamFieldNames = "[class^='TreeRowWrapper_rowWrapper'] span"; -const streamDataTypes = "[class^='TreeRowWrapper_rowWrapper'] div:nth-child(2)"; -const ExpandStreamDetailsTableBtn = "[class^='Arrow_container__']"; -const cursorFieldText = "[class^='PathPopoutButton_button__']"; -const primaryKeyText = "[class^='PathPopoutButton_button__']"; -const preFilledPrimaryKeyText = "div[class^='PathPopout_text']"; -const primaryKeyDropdown = "button[class^='PathPopoutButton_button']"; +const getFieldDropdownContainer = (streamName: string, type: Dropdown) => `div[id='${streamName}_${type}_pathPopout']`; +const getFieldDropdownButton = (streamName: string, type: Dropdown) => + `button[data-testid='${streamName}_${type}_pathPopout']`; +const getFieldDropdownOption = (value: string) => `div[data-testid='${value}']`; +const dropDownOverlayContainer = "div[data-testid='overlayContainer']"; +const streamNameCell = "[data-testid='nameCell']"; +const streamDataTypeCell = "[data-testid='dataTypeCell']"; +const getExpandStreamArrowBtn = (streamName: string) => `[data-testid='${streamName}_expandStreamDetails']`; +const getPreFilledPrimaryKeyText = (streamName: string) => `[data-testid='${streamName}_primaryKey_pathPopout_text']`; const successResult = "div[data-id='success-result']"; const saveStreamChangesButton = "button[data-testid='resetModal-save']"; const connectionNameInput = "input[data-testid='connectionName']"; const refreshSourceSchemaButton = "button[data-testid='refresh-source-schema-btn']"; const streamSyncEnabledSwitch = (streamName: string) => `[data-testid='${streamName}-stream-sync-switch']`; const streamNameInput = "input[data-testid='input']"; +const resetModalSaveButton = "[data-testid='resetModal-save']"; export const goToReplicationTab = () => { cy.get(replicationTab).click(); @@ -31,9 +33,7 @@ export const enterConnectionName = (name: string) => { cy.get(connectionNameInput).type(name); }; -export const expandStreamDetails = () => { - cy.get(ExpandStreamDetailsTableBtn).click(); -}; +export const expandStreamDetailsByName = (streamName: string) => cy.get(getExpandStreamArrowBtn(streamName)).click(); export const selectSchedule = (value: string) => { cy.get(scheduleDropdown).click(); @@ -55,13 +55,9 @@ export const setupDestinationNamespaceSourceFormat = () => { cy.get(destinationNamespaceSource).click(); }; -export const refreshSourceSchemaBtnClick = () => { - cy.get(refreshSourceSchemaButton).click(); -}; +export const refreshSourceSchemaBtnClick = () => cy.get(refreshSourceSchemaButton).click(); -export const resetModalSaveBtnClick = () => { - cy.get("[data-testid='resetModal-save']").click(); -}; +export const resetModalSaveBtnClick = () => cy.get(resetModalSaveButton).click(); export const setupDestinationNamespaceDefaultFormat = () => { cy.get(destinationNamespace).click(); @@ -74,42 +70,100 @@ export const selectSyncMode = (source: string, dest: string) => { cy.get(`.react-select__option`).contains(`Source:${source}|Dest:${dest}`).click(); }; -export const selectCursorField = (value: string) => { - cy.get(cursorFieldDropdown).first().click({ force: true }); - - cy.get(`.react-select__option`).contains(value).click(); +type Dropdown = "cursor" | "primaryKey"; +/** + * General function - select dropdown option(s) + * @param streamName + * @param dropdownType + * @param value + */ +const selectFieldDropdownOption = (streamName: string, dropdownType: Dropdown, value: string | string[]) => { + const container = getFieldDropdownContainer(streamName, dropdownType); + const button = getFieldDropdownButton(streamName, dropdownType); + + cy.get(container).within(() => { + cy.get(button).click(); + + if (Array.isArray(value)) { + // in case if multiple options need to be selected + value.forEach((v) => cy.get(getFieldDropdownOption(v)).click()); + } else { + // in case if one option need to be selected + cy.get(getFieldDropdownOption(value)).click(); + } + }); + // close dropdown + // (dropdown need to be closed manually by clicking on overlay in case if multiple option selection is available) + cy.get("body").then(($body) => { + if ($body.find(dropDownOverlayContainer).length > 0) { + cy.get(dropDownOverlayContainer).click(); + } + }); }; -export const checkStreamFields = (listNames: string[], listTypes: string[]) => { - cy.get(streamFieldNames).each(($span, i) => { +/** + * Select cursor value from cursor dropdown(pathPopout) in desired stream + * @param streamName + * @param cursorValue + */ +export const selectCursorField = (streamName: string, cursorValue: string) => + selectFieldDropdownOption(streamName, "cursor", cursorValue); + +/** + * Select primary key value(s) from primary key dropdown(pathPopout) in desired stream + * @param streamName + * @param primaryKeyValues + */ +export const selectPrimaryKeyField = (streamName: string, primaryKeyValues: string[]) => + selectFieldDropdownOption(streamName, "primaryKey", primaryKeyValues); + +export const checkStreamFields = (listNames: Array, listTypes: Array) => { + cy.get(streamNameCell).each(($span, i) => { expect($span.text()).to.equal(listNames[i]); }); - cy.get(streamDataTypes).each(($span, i) => { + cy.get(streamDataTypeCell).each(($span, i) => { expect($span.text()).to.equal(listTypes[i]); }); }; -export const checkCursorField = (expectedValue: string) => { - cy.get(cursorFieldText).first().contains(expectedValue); -}; +/** + * General function - check selected field dropdown option or options + * @param streamName + * @param dropdownType + * @param expectedValue + */ +const checkDropdownField = (streamName: string, dropdownType: Dropdown, expectedValue: string | string[]) => { + const button = getFieldDropdownButton(streamName, dropdownType); + const isButtonContainsExactValue = (value: string) => cy.get(button).contains(new RegExp(`^${value}$`)); -export const checkPrimaryKey = (expectedValue: string) => { - cy.get(primaryKeyText).last().contains(expectedValue); + return Array.isArray(expectedValue) + ? expectedValue.every((value) => isButtonContainsExactValue(value)) + : isButtonContainsExactValue(expectedValue); }; -export const checkPreFilledPrimaryKeyField = (expectedValue: string) => { - cy.get(preFilledPrimaryKeyText).contains(expectedValue); -}; +/** + * Check selected value in cursor dropdown + * @param streamName + * @param expectedValue + */ +export const checkCursorField = (streamName: string, expectedValue: string) => + checkDropdownField(streamName, "cursor", expectedValue); -export const isPrimaryKeyNonExist = () => { - cy.get(preFilledPrimaryKeyText).should("not.exist"); -}; +/** + * Check selected value(s) in primary key dropdown + * @param streamName + * @param expectedValues + */ +export const checkPrimaryKey = (streamName: string, expectedValues: string[]) => + checkDropdownField(streamName, "primaryKey", expectedValues); -export const selectPrimaryKeyField = (value: string) => { - cy.get(primaryKeyDropdown).last().click({ force: true }); +export const checkPreFilledPrimaryKeyField = (streamName: string, expectedValue: string) => { + cy.get(getPreFilledPrimaryKeyText(streamName)).contains(expectedValue); +}; - cy.get(`.react-select__option`).contains(value).click(); +export const isPrimaryKeyNonExist = (streamName: string) => { + cy.get(getPreFilledPrimaryKeyText(streamName)).should("not.exist"); }; export const searchStream = (value: string) => { diff --git a/airbyte-webapp-e2e-tests/cypress/pages/settingsConnectionPage.ts b/airbyte-webapp-e2e-tests/cypress/pages/settingsConnectionPage.ts index 9fcd478dfa38..5c4b1c00d568 100644 --- a/airbyte-webapp-e2e-tests/cypress/pages/settingsConnectionPage.ts +++ b/airbyte-webapp-e2e-tests/cypress/pages/settingsConnectionPage.ts @@ -1,4 +1,17 @@ +import { clickOnCellInTable } from "commands/common"; + const settingsTab = "div[data-id='settings-step']"; +const sourceColumnName = "Source name"; +const destinationColumnName = "Destination name"; +const connectionsTable = "table[data-testid='connectionsTable']"; + +export const openConnectionOverviewBySourceName = (sourceName: string) => { + clickOnCellInTable(connectionsTable, sourceColumnName, sourceName); +}; + +export const openConnectionOverviewByDestinationName = (destinationName: string) => { + clickOnCellInTable(connectionsTable, destinationColumnName, destinationName); +}; export const goToSettingsPage = () => { cy.get(settingsTab).click(); diff --git a/airbyte-webapp-e2e-tests/cypress/pages/sourcePage.ts b/airbyte-webapp-e2e-tests/cypress/pages/sourcePage.ts index 5150322b1556..ed80ecebb866 100644 --- a/airbyte-webapp-e2e-tests/cypress/pages/sourcePage.ts +++ b/airbyte-webapp-e2e-tests/cypress/pages/sourcePage.ts @@ -1,4 +1,8 @@ +import { clickOnCellInTable } from "commands/common"; + const newSource = "button[data-id='new-source']"; +const sourcesTable = "table[data-testid='sourcesTable']"; +const sourceNameColumn = "Name"; export const goToSourcePage = () => { cy.intercept("/api/v1/sources/list").as("getSourcesList"); @@ -10,6 +14,10 @@ export const openSourceDestinationFromGrid = (value: string) => { cy.get("div").contains(value).click(); }; +export const openSourceOverview = (sourceName: string) => { + clickOnCellInTable(sourcesTable, sourceNameColumn, sourceName); +}; + export const openNewSourceForm = () => { cy.wait("@getSourcesList").then(({ response }) => { if (response?.body.sources.length) { diff --git a/airbyte-webapp/src/components/EntityTable/ConnectionTable.tsx b/airbyte-webapp/src/components/EntityTable/ConnectionTable.tsx index e57b1e15bc05..9285818c30ce 100644 --- a/airbyte-webapp/src/components/EntityTable/ConnectionTable.tsx +++ b/airbyte-webapp/src/components/EntityTable/ConnectionTable.tsx @@ -178,7 +178,7 @@ const ConnectionTable: React.FC = ({ data, entity, onClickRow, onSync }) [sortBy, sortOrder, entity, onSortClick, onSync, allowSync, allowAutoDetectSchema] ); - return ; + return
; }; export default ConnectionTable; diff --git a/airbyte-webapp/src/components/EntityTable/ImplementationTable.tsx b/airbyte-webapp/src/components/EntityTable/ImplementationTable.tsx index 6c06dee979a6..ab55ba515206 100644 --- a/airbyte-webapp/src/components/EntityTable/ImplementationTable.tsx +++ b/airbyte-webapp/src/components/EntityTable/ImplementationTable.tsx @@ -132,7 +132,7 @@ const ImplementationTable: React.FC = ({ data, entity, onClickRow }) => return (
-
+
); }; diff --git a/airbyte-webapp/src/components/connection/CatalogTree/Arrow.tsx b/airbyte-webapp/src/components/connection/CatalogTree/Arrow.tsx index 39b0d21f28e1..6bb389fdb008 100644 --- a/airbyte-webapp/src/components/connection/CatalogTree/Arrow.tsx +++ b/airbyte-webapp/src/components/connection/CatalogTree/Arrow.tsx @@ -12,9 +12,9 @@ interface ArrowProps { onExpand?: () => void; } -const Arrow: React.FC = ({ isItemHasChildren, isItemOpen, onExpand }) => { +const Arrow: React.FC = ({ isItemHasChildren, isItemOpen, onExpand, ...restProps }) => { return ( - + {(isItemHasChildren || !onExpand) && ( > = ({ children }) => { - return {children}; +const DataTypeCell: React.FC> = ({ children, ...restProps }) => { + return {children}; }; export default DataTypeCell; diff --git a/airbyte-webapp/src/components/connection/CatalogTree/FieldRow.tsx b/airbyte-webapp/src/components/connection/CatalogTree/FieldRow.tsx index f1904ed1a755..c2fd76148656 100644 --- a/airbyte-webapp/src/components/connection/CatalogTree/FieldRow.tsx +++ b/airbyte-webapp/src/components/connection/CatalogTree/FieldRow.tsx @@ -98,15 +98,19 @@ const FieldRowInner: React.FC = ({ )} {isColumnSelectionEnabled && ( - {name} + + {name} + )} {!isColumnSelectionEnabled && ( - {name} + + {name} + )} - {dataType} + {dataType} {shouldDefineCursor && onCursorChange(field.path)} />} diff --git a/airbyte-webapp/src/components/connection/CatalogTree/PathPopout.tsx b/airbyte-webapp/src/components/connection/CatalogTree/PathPopout.tsx index 3e1bae6df130..dfb6ffbdfbab 100644 --- a/airbyte-webapp/src/components/connection/CatalogTree/PathPopout.tsx +++ b/airbyte-webapp/src/components/connection/CatalogTree/PathPopout.tsx @@ -19,6 +19,7 @@ interface PathPopoutBaseProps { paths: Path[]; pathType: "required" | "sourceDefined"; placeholder?: React.ReactNode; + id?: string; } interface PathMultiProps { @@ -36,12 +37,21 @@ interface PathProps { type PathPopoutProps = PathPopoutBaseProps & (PathMultiProps | PathProps); export const PathPopout: React.FC = (props) => { + const pathPopoutId = `${props.id}_pathPopout`; + if (props.pathType === "sourceDefined") { if (props.path && props.path.length > 0) { const text = props.isMulti ? props.path.map(pathDisplayName).join(", ") : pathDisplayName(props.path); return ( - {text}}> + + {text} + + } + > {text} ); @@ -74,8 +84,13 @@ export const PathPopout: React.FC = (props) => { }} placeholder={props.placeholder} components={props.isMulti ? { MultiValue: () => null } : undefined} + id={pathPopoutId} targetComponent={({ onOpen }) => ( - + {text} )} diff --git a/airbyte-webapp/src/components/connection/CatalogTree/PathPopoutButton.tsx b/airbyte-webapp/src/components/connection/CatalogTree/PathPopoutButton.tsx index b7c685c5870b..1af6d7af3e1c 100644 --- a/airbyte-webapp/src/components/connection/CatalogTree/PathPopoutButton.tsx +++ b/airbyte-webapp/src/components/connection/CatalogTree/PathPopoutButton.tsx @@ -10,16 +10,18 @@ import styles from "./PathPopoutButton.module.scss"; interface PathPopoutButtonProps { items?: string[]; onClick: React.MouseEventHandler; + testId?: string; } export const PathPopoutButton: React.FC> = ({ items = [], onClick, children, + testId, }) => ( + {headerGroups.map((headerGroup, key) => ( diff --git a/airbyte-webapp/src/pages/connections/ConnectionReplicationPage/__snapshots__/ConnectionReplicationPage.test.tsx.snap b/airbyte-webapp/src/pages/connections/ConnectionReplicationPage/__snapshots__/ConnectionReplicationPage.test.tsx.snap index 00fabefa1648..685a07a2f217 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionReplicationPage/__snapshots__/ConnectionReplicationPage.test.tsx.snap +++ b/airbyte-webapp/src/pages/connections/ConnectionReplicationPage/__snapshots__/ConnectionReplicationPage.test.tsx.snap @@ -648,6 +648,7 @@ exports[`ConnectionReplicationPage should render 1`] = ` >
pokemon