From 96634b06b70670e5c215b89f0be7bcf42cccba13 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 15 Aug 2023 15:33:50 +0800 Subject: [PATCH] test: add ui tests for social connectors (#4328) --- packages/console/src/utils/connector-form.ts | 2 +- .../connectors/social-connector-test-cases.ts | 370 ++++++++++++++++++ .../ui/connectors/social-connectors.test.ts | 199 ++++++++++ .../integration-tests/src/ui-helpers/index.ts | 32 ++ 4 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 packages/integration-tests/src/tests/ui/connectors/social-connector-test-cases.ts create mode 100644 packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts diff --git a/packages/console/src/utils/connector-form.ts b/packages/console/src/utils/connector-form.ts index dc5eb4238..63583b119 100644 --- a/packages/console/src/utils/connector-form.ts +++ b/packages/console/src/utils/connector-form.ts @@ -71,7 +71,7 @@ export const convertResponseToForm = (connector: ConnectorResponse): ConnectorFo logo, logoDark, target: conditional( - type === ConnectorType.Social && !isStandard && (metadata.target ?? target) + type === ConnectorType.Social && (isStandard ? target : metadata.target ?? target) ), syncProfile: syncProfile ? SyncProfileMode.EachSignIn : SyncProfileMode.OnlyAtRegister, jsonConfig: JSON.stringify(config, null, 2), diff --git a/packages/integration-tests/src/tests/ui/connectors/social-connector-test-cases.ts b/packages/integration-tests/src/tests/ui/connectors/social-connector-test-cases.ts new file mode 100644 index 000000000..4cc55d5a4 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/connectors/social-connector-test-cases.ts @@ -0,0 +1,370 @@ +/* eslint-disable max-lines */ +export type SocialConnectorCase = { + groupFactoryId?: string; + factoryId: string; + name: string; + initialFormData: Record; + updateFormData: Record; + errorFormData: Record; + standardBasicFormData?: Record; +}; + +const google: SocialConnectorCase = { + factoryId: 'google-universal', + name: 'Google', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + 'formConfig.scope': '', + }, +}; + +const apple: SocialConnectorCase = { + factoryId: 'apple-universal', + name: 'Apple', + initialFormData: { + 'formConfig.clientId': 'client-id', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + }, + errorFormData: { + 'formConfig.clientId': '', + }, +}; + +const facebook: SocialConnectorCase = { + factoryId: 'facebook-universal', + name: 'Facebook', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + 'formConfig.scope': '', + }, +}; + +const github: SocialConnectorCase = { + factoryId: 'github-universal', + name: 'GitHub', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + 'formConfig.scope': '', + }, +}; + +const discord: SocialConnectorCase = { + factoryId: 'discord-universal', + name: 'Discord', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + 'formConfig.scope': '', + }, +}; + +const kakao: SocialConnectorCase = { + factoryId: 'kakao-universal', + name: 'Kakao', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + }, +}; + +const naver: SocialConnectorCase = { + factoryId: 'naver-universal', + name: 'Naver', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + }, +}; + +const microsoft: SocialConnectorCase = { + factoryId: 'azuread-universal', + name: 'Microsoft', + initialFormData: { + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret', + 'formConfig.cloudInstance': 'cloud-instance', + 'formConfig.tenantId': 'tenant-id', + }, + updateFormData: { + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret', + 'formConfig.cloudInstance': 'new-cloud-instance', + 'formConfig.tenantId': 'new-tenant-id', + }, + errorFormData: { + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + 'formConfig.cloudInstance': '', + 'formConfig.tenantId': '', + }, +}; + +const feishu: SocialConnectorCase = { + factoryId: 'feishu-web', + name: 'Feishu', + initialFormData: { + 'formConfig.appId': 'app-id', + 'formConfig.appSecret': 'app-secret', + }, + updateFormData: { + 'formConfig.appId': 'new-app-id', + 'formConfig.appSecret': 'new-app-secret', + }, + errorFormData: { + 'formConfig.appId': '', + 'formConfig.appSecret': '', + }, +}; + +const wechatNative: SocialConnectorCase = { + groupFactoryId: 'wechat-native', + factoryId: 'wechat-native', + name: 'WeChat', + initialFormData: { + 'formConfig.appId': 'app-id', + 'formConfig.appSecret': 'app-secret', + 'formConfig.universalLinks': 'universal-links', + }, + updateFormData: { + 'formConfig.appId': 'new-app-id', + 'formConfig.appSecret': 'new-app-secret', + 'formConfig.universalLinks': 'new-universal-links', + }, + errorFormData: { + 'formConfig.appId': '', + 'formConfig.appSecret': '', + 'formConfig.universalLinks': '', + }, +}; + +const wechatWeb: SocialConnectorCase = { + groupFactoryId: 'wechat-native', + factoryId: 'wechat-web', + name: 'WeChat', + initialFormData: { + 'formConfig.appId': 'app-id', + 'formConfig.appSecret': 'app-secret', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.appId': 'new-app-id', + 'formConfig.appSecret': 'new-app-secret', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.appId': '', + 'formConfig.appSecret': '', + 'formConfig.scope': '', + }, +}; + +const alipayNative: SocialConnectorCase = { + groupFactoryId: 'alipay-native', + factoryId: 'alipay-native', + name: 'Alipay', + initialFormData: { + 'formConfig.appId': 'app-id', + 'formConfig.privateKey': 'private-key', + }, + updateFormData: { + 'formConfig.appId': 'new-app-id', + 'formConfig.privateKey': 'new-private-key', + }, + errorFormData: { + 'formConfig.appId': '', + 'formConfig.privateKey': '', + }, +}; + +const alipayWeb: SocialConnectorCase = { + groupFactoryId: 'alipay-native', + factoryId: 'alipay-web', + name: 'Alipay', + initialFormData: { + 'formConfig.appId': 'app-id', + 'formConfig.privateKey': 'private-key', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.appId': 'new-app-id', + 'formConfig.privateKey': 'new-private-key', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.appId': '', + 'formConfig.privateKey': '', + 'formConfig.scope': '', + }, +}; + +const oauth2: SocialConnectorCase = { + factoryId: 'oauth2', + name: 'OAuth 2.0', + initialFormData: { + 'formConfig.authorizationEndpoint': 'authorization-endpoint', + 'formConfig.tokenEndpoint': 'token-endpoint', + 'formConfig.userInfoEndpoint': 'user-info-endpoint', + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret-id', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.authorizationEndpoint': 'new-authorization-endpoint', + 'formConfig.tokenEndpoint': 'new-token-endpoint', + 'formConfig.userInfoEndpoint': 'new-user-info-endpoint', + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret-id', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.authorizationEndpoint': '', + 'formConfig.tokenEndpoint': '', + 'formConfig.userInfoEndpoint': '', + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + 'formConfig.scope': '', + }, + standardBasicFormData: { + name: 'OAuth 2.0', + target: 'oauth2', + }, +}; + +const oidc: SocialConnectorCase = { + factoryId: 'oidc', + name: 'OIDC', + initialFormData: { + 'formConfig.authorizationEndpoint': 'authorization-endpoint', + 'formConfig.tokenEndpoint': 'token-endpoint', + 'formConfig.clientId': 'client-id', + 'formConfig.clientSecret': 'client-secret-id', + 'formConfig.scope': 'scope', + }, + updateFormData: { + 'formConfig.authorizationEndpoint': 'new-authorization-endpoint', + 'formConfig.tokenEndpoint': 'new-token-endpoint', + 'formConfig.clientId': 'new-client-id', + 'formConfig.clientSecret': 'new-client-secret-id', + 'formConfig.scope': 'new-scope', + }, + errorFormData: { + 'formConfig.authorizationEndpoint': '', + 'formConfig.tokenEndpoint': '', + 'formConfig.clientId': '', + 'formConfig.clientSecret': '', + }, + standardBasicFormData: { + name: 'OIDC', + target: 'oidc', + }, +}; + +const saml: SocialConnectorCase = { + factoryId: 'saml', + name: 'SAML', + initialFormData: { + 'formConfig.entityID': 'entity-id', + 'formConfig.signInEndpoint': 'sign-in-endpoint', + 'formConfig.x509Certificate': 'x509-certificate', + 'formConfig.idpMetadataXml': 'idp-metadata-xml', + 'formConfig.assertionConsumerServiceUrl': 'assertion-consumer-service-url', + }, + updateFormData: { + 'formConfig.entityID': 'new-entity-id', + 'formConfig.signInEndpoint': 'new-sign-in-endpoint', + 'formConfig.x509Certificate': 'new-x509-certificate', + 'formConfig.idpMetadataXml': 'new-idp-metadata-xml', + 'formConfig.assertionConsumerServiceUrl': 'new-assertion-consumer-service-url', + }, + errorFormData: { + 'formConfig.entityID': '', + 'formConfig.signInEndpoint': '', + 'formConfig.x509Certificate': '', + 'formConfig.idpMetadataXml': '', + 'formConfig.assertionConsumerServiceUrl': '', + }, + standardBasicFormData: { + name: 'SAML', + target: 'saml', + }, +}; +export const socialConnectorTestCases = [ + google, + apple, + facebook, + github, + discord, + kakao, + naver, + microsoft, + feishu, + wechatNative, + wechatWeb, + alipayNative, + alipayWeb, + oauth2, + oidc, + saml, +]; +/* eslint-enable max-lines */ diff --git a/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts b/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts new file mode 100644 index 000000000..53d2f5f2a --- /dev/null +++ b/packages/integration-tests/src/tests/ui/connectors/social-connectors.test.ts @@ -0,0 +1,199 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { + expectUnsavedChangesAlert, + goToAdminConsole, + trySaveChanges, + waitForSuccessToast, +} from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { + socialConnectorTestCases, + type SocialConnectorCase, +} from './social-connector-test-cases.js'; + +await page.setViewport({ width: 1920, height: 1080 }); + +describe('social connectors', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + + beforeAll(async () => { + await goToAdminConsole(); + }); + + it('navigate to social connector page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/connectors/social', logtoConsoleUrl).href) + ); + + await expect(page).toMatchElement( + 'div[class$=main] div[class$=headline] div[class$=titleEllipsis]', + { + text: 'Connectors', + } + ); + + await expect(page).toMatchElement('nav div[class$=item] div[class$=selected] a', { + text: 'Social connectors', + }); + + expect(page.url()).toBe(new URL(`console/connectors/social`, logtoConsoleUrl).href); + }); + + it('can open create connector modal from table placeholder', async () => { + await expect(page).toClick('table div[class$=placeholder] button span', { + text: 'Add Social Connector', + }); + + await expect(page).toMatchElement( + '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', + { + text: 'Add Social Connector', + } + ); + + // Close modal + await page.keyboard.press('Escape'); + }); + + it.each(socialConnectorTestCases)( + 'can create and modify a(n) $factoryId social connector', + async ({ + groupFactoryId, + factoryId, + name, + initialFormData, + updateFormData, + errorFormData, + standardBasicFormData, + }: SocialConnectorCase) => { + await expect(page).toClick('div[class$=headline] button[class$=withIcon] span', { + text: 'Add Social Connector', + }); + + await expect(page).toMatchElement( + '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', + { + text: 'Add Social Connector', + } + ); + + if (groupFactoryId) { + // Platform selector + await page.click( + `.ReactModalPortal div[role=radio]:has(input[name=group][value=${groupFactoryId}])` + ); + + await page.waitForSelector( + '.ReactModalPortal div[class$=platforms] div[class$=radioGroup]' + ); + + await page.click( + `.ReactModalPortal div[class$=platforms] div[role=radio]:has(input[name=connector][value=${factoryId}])` + ); + } else { + await page.click( + `.ReactModalPortal div[role=radio]:has(input[name=group][value=${factoryId}])` + ); + } + + await expect(page).toClick('.ReactModalPortal div[class$=footer] button:not(disabled) span', { + text: 'Next', + }); + + await expect(page).toMatchElement('.ReactModalPortal div[class$=titleEllipsis] span', { + text: name, + }); + + await expect(page).toMatchElement('.ReactModalPortal div[class$=subtitle] span', { + text: 'A step by step guide to configure your connector', + }); + + await expect(page).toClick( + '.ReactModalPortal form div[class$=footer] button[type=submit] span', + { + text: 'Save and Done', + } + ); + + // Display error input + await page.waitForSelector('form div[class$=field] div[class$=error]'); + + await expect(page).toFillForm('.ReactModalPortal form', { + ...standardBasicFormData, + ...initialFormData, + }); + + await expect(page).toClick( + '.ReactModalPortal form div[class$=footer] button[type=submit] span', + { + text: 'Save and Done', + } + ); + + await waitForSuccessToast(page, 'Saved'); + + await expect(page).toMatchElement('div[class$=header] div[class$=name] span', { + text: name, + }); + + // Fill incorrect form + await expect(page).toFillForm('form', errorFormData); + + await trySaveChanges(page); + + await page.waitForSelector('form div[class$=field] div[class$=error]'); + + // Update form + await expect(page).toFillForm('form', updateFormData); + + await expectUnsavedChangesAlert(page); + + await trySaveChanges(page); + + await waitForSuccessToast(page, 'Saved'); + + // Delete connector + await expect(page).toClick( + 'div[class$=header] div[class$=operations] button[class$=withIcon]:has(span[class$=icon] > svg[class$=moreIcon])' + ); + + await expect(page).toMatchElement( + '.ReactModalPortal div[class$=dropdownContainer] div[class$=dropdownTitle]', + { + text: 'MORE OPTIONS', + } + ); + + // Wait for the dropdown menu to be rendered in the correct position + await page.waitForTimeout(500); + + await expect(page).toClick( + '.ReactModalPortal div[class$=dropdownContainer] div[role=menuitem]', + { text: 'Delete' } + ); + + await page.waitForSelector( + '.ReactModalPortal div[class$=dropdownContainer] div[class$=dropdownTitle]', + { + hidden: true, + } + ); + + await expect(page).toMatchElement( + '.ReactModalPortal div[class$=header] div[class$=titleEllipsis]', + { + text: 'Reminder', + } + ); + + await expect(page).toClick('.ReactModalPortal div[class$=footer] button span', { + text: 'Delete', + }); + + await waitForSuccessToast(page, 'The connector has been successfully deleted'); + + expect(page.url()).toBe(new URL(`console/connectors/social`, logtoConsoleUrl).href); + } + ); +}); diff --git a/packages/integration-tests/src/ui-helpers/index.ts b/packages/integration-tests/src/ui-helpers/index.ts index 915da2d90..ee99189f4 100644 --- a/packages/integration-tests/src/ui-helpers/index.ts +++ b/packages/integration-tests/src/ui-helpers/index.ts @@ -1,3 +1,5 @@ +import { type Page } from 'puppeteer'; + import { consolePassword, consoleUsername, @@ -17,3 +19,33 @@ export const goToAdminConsole = async () => { await expectNavigation(expect(page).toClick('button[name=submit]')); } }; + +export const waitForSuccessToast = async (page: Page, text: string) => { + const successToastHandle = await page.waitForSelector('div[class*=toast][class*=success]'); + await expect(successToastHandle).toMatchElement('div[class$=message]', { + text, + }); + // Wait the success toast to disappear so that the next time we call this function we will match the brand new toast + await page.waitForSelector('div[class*=toast][class*=success]', { + hidden: true, + }); +}; + +export const expectUnsavedChangesAlert = async (page: Page) => { + // Unsaved changes alert + await page.goBack(); + + await page.waitForSelector( + '.ReactModalPortal div[class$=content]::-p-text(You have made some changes. Are you sure you want to leave this page?)' + ); + + await expect(page).toClick('.ReactModalPortal div[class$=footer] button', { + text: 'Stay on Page', + }); +}; + +export const trySaveChanges = async (page: Page) => { + // Wait for the action bar to finish animating + await page.waitForTimeout(500); + await expect(page).toClick('div[class$=actionBar] button span', { text: 'Save Changes' }); +};