E2E tests for admin portal SSO (#846)

* Sync lock file

* Add admin portal tests

* Use data-testid to enable `page.getByTestId()`

* Support data-testid for inner elements

* Pass data-testid to sso login button

* Remove env-cmd

* Update folder structure

* Add data-testid

* - Use production build for local testing
 - Set NODE_ENV to pick up .env.test.local

* Add test-id for logout dropdown activation

* Remove test; superseded with new tests

* Fix tests

* Wait for navigation to complete

* [Failing ci test try fix] Pass url for waiting

* [Failing ci test try fix] skip assertion

* [Fix failing test] use predicate to match origin

* Add back the visibility check

* Fix query param

* Tweak test code

* Update return type of callback

* Group tests and reorganise folders

* Add actions to login and assert by locator

* Support html attribute passing

* Set data-testid on edit button

* Set data-testids

* Final changes

* Tweak
This commit is contained in:
Aswin V 2023-01-20 16:07:01 +05:30 committed by GitHub
parent 43ea311067
commit 305ff93cbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 87 additions and 23 deletions

View File

@ -18,7 +18,9 @@ const ConfirmationModal = (props: {
<Modal visible={visible} title={title} description={description}>
<div className='modal-action'>
<ButtonOutline onClick={onCancel}>{t('cancel')}</ButtonOutline>
<ButtonDanger onClick={onConfirm}>{actionButtonText || t('delete')}</ButtonDanger>
<ButtonDanger onClick={onConfirm} data-testid='confirm-delete'>
{actionButtonText || t('delete')}
</ButtonDanger>
</div>
</Modal>
);

View File

@ -1,11 +1,12 @@
import classNames from 'classnames';
export const IconButton = ({ Icon, tooltip, onClick, className }) => {
export const IconButton = ({ Icon, tooltip, onClick, className, ...other }) => {
return (
<div className='tooltip' data-tip={tooltip}>
<Icon
className={classNames('hover:scale-115 h-5 w-5 cursor-pointer text-secondary', className)}
onClick={onClick}
{...other}
/>
</div>
);

View File

@ -22,6 +22,7 @@ export const Navbar = ({ session }: { session: Session | null }) => {
className='flex h-8 w-8 items-center justify-center rounded-full bg-secondary uppercase text-cyan-50 focus:outline-none'
aria-expanded='false'
aria-haspopup='true'
data-testid='user-avatar'
onClick={() => {
setIsOpen(!isOpen);
}}>
@ -45,6 +46,7 @@ export const Navbar = ({ session }: { session: Session | null }) => {
className='link flex px-4 py-2 text-sm hover:link-primary'
role='menuitem'
tabIndex={-1}
data-testid='logout'
id='user-menu-item-2'
onClick={() => signOut()}>
<PowerIcon className='mr-1 h-5 w-5' aria-hidden />

View File

@ -72,14 +72,14 @@ const ConnectionList = ({
{t(isSettingsView ? 'admin_portal_sso' : 'enterprise_sso')}
</h2>
<div className='flex gap-2'>
<LinkPrimary Icon={PlusIcon} href={createConnectionUrl} data-test-id='create-connection'>
<LinkPrimary Icon={PlusIcon} href={createConnectionUrl} data-testid='create-connection'>
{t('new_connection')}
</LinkPrimary>
{!setupLinkToken && !isSettingsView && (
<LinkPrimary
Icon={LinkIcon}
href='/admin/sso-connection/setup-link/new'
data-test-id='create-setup-link'>
data-testid='create-setup-link'>
{t('new_setup_link')}
</LinkPrimary>
)}
@ -165,6 +165,7 @@ const ConnectionList = ({
tooltip={t('edit')}
Icon={PencilIcon}
className='hover:text-green-400'
data-testid='edit'
onClick={() => {
router.push(
setupLinkToken

View File

@ -149,7 +149,9 @@ const CreateConnection = ({
.filter(({ attributes: { hideInSetupView } }) => (setupLinkToken ? !hideInSetupView : true))
.map(renderFieldList({ formObj, setFormObj }))}
<div className='flex'>
<ButtonPrimary loading={loading}>{t('save_changes')}</ButtonPrimary>
<ButtonPrimary loading={loading} data-testid='submit-form-create-sso'>
{t('save_changes')}
</ButtonPrimary>
</div>
</div>
</form>

View File

@ -171,7 +171,11 @@ const EditConnection = ({ connection, setupLinkToken, isSettingsView = false }:
<h6 className='mb-1 font-medium'>{t('delete_this_connection')}</h6>
<p className='font-light'>{t('all_your_apps_using_this_connection_will_stop_working')}</p>
</div>
<ButtonDanger type='button' onClick={toggleDelConfirm} data-modal-toggle='popup-modal'>
<ButtonDanger
type='button'
onClick={toggleDelConfirm}
data-modal-toggle='popup-modal'
data-testid='delete-connection'>
{t('delete')}
</ButtonDanger>
</section>

View File

@ -15,7 +15,7 @@ export const saveConnection = async ({
connectionIsSAML: boolean;
connectionIsOIDC: boolean;
setupLinkToken?: string;
callback: (res: Response) => void;
callback: (res: Response) => Promise<void>;
}) => {
const { rawMetadata, redirectUrl, oidcDiscoveryUrl, oidcClientId, oidcClientSecret, metadataUrl, ...rest } =
formObj;

View File

@ -119,7 +119,7 @@ const SetupLinkList = ({ service }: { service: SetupLinkService }) => {
<div className='mb-5 flex items-center justify-between'>
<h3>{description}</h3>
<div>
<LinkPrimary Icon={PlusIcon} href={createSetupLinkUrl} data-test-id='create-setup-link'>
<LinkPrimary Icon={PlusIcon} href={createSetupLinkUrl} data-testid='create-setup-link'>
{t('new_setup_link')}
</LinkPrimary>
</div>

View File

@ -1,8 +0,0 @@
import { test } from '@playwright/test';
test('MAGIC_LINK in globalSetup should log me in', async ({ page }) => {
await page.goto('/admin/sso-connection');
// Find the button and click on it
await page.locator('data-test-id=create-connection').click();
});

View File

@ -0,0 +1,54 @@
import { expect, test } from '@playwright/test';
const TEST_SSO_CONNECTION_NAME = 'pw_admin_portal_sso';
const MOCKSAML_ORIGIN = 'https://mocksaml.com';
const MOCKSAML_METADATA_URL = `${MOCKSAML_ORIGIN}/api/saml/metadata`;
const MOCKSAML_SIGNIN_BUTTON_NAME = 'Sign In';
test.describe('Admin Portal SSO', () => {
test('should be able to add SSO connection to mocksaml.com', async ({ page }) => {
await page.goto('/admin/settings');
// Find the new connection button and click on it
await page.getByTestId('create-connection').click();
// Fill the name for the connection
const nameInput = page.locator('#name');
await nameInput.fill(TEST_SSO_CONNECTION_NAME);
// Enter the metadata url for mocksaml.com in the form
const metadataUrlInput = page.locator('#metadataUrl');
await metadataUrlInput.fill(MOCKSAML_METADATA_URL);
// submit the form
await page.getByTestId('submit-form-create-sso').click();
// check if the added connection appears in the connection list
await expect(page.getByText(TEST_SSO_CONNECTION_NAME)).toBeVisible();
});
test('should be able to login via mocksaml.com SSO', async ({ page, baseURL }) => {
const userAvatarLocator = page.getByTestId('user-avatar');
// Logout from the magic link authentication
await page.goto('/');
await userAvatarLocator.click();
await page.getByTestId('logout').click();
// Click on login with sso button
await page.getByTestId('sso-login-button').click();
// Perform sign in at mocksaml
await page.waitForURL((url) => url.origin === MOCKSAML_ORIGIN);
await page.getByPlaceholder('jackson').fill('bob');
await page.getByRole('button', { name: MOCKSAML_SIGNIN_BUTTON_NAME }).click();
// Wait for browser to redirect back to admin portal
await page.waitForURL((url) => url.origin === baseURL);
// assert login state
await expect(userAvatarLocator).toBeVisible();
});
test('delete the SSO connection', async ({ page }) => {
await page.goto('/admin/settings');
// select the row of the connection list table, then locate the edit button
const editButton = page.getByText(TEST_SSO_CONNECTION_NAME).locator('..').getByTestId('edit');
await editButton.click();
// click the delete and confirm deletion
await page.getByTestId('delete-connection').click();
await page.getByTestId('confirm-delete').click();
// check that the SSO connection is deleted from the connection list
await expect(page.getByText(TEST_SSO_CONNECTION_NAME)).not.toBeVisible();
});
});

View File

@ -33,7 +33,7 @@
"prepare:npm": "cd npm && npm install --legacy-peer-deps",
"prepare:sdk": "cd sdk/ui/react && npm install",
"pretest:e2e": "env-cmd -f .env.test.local ts-node --log-error e2e/seedAuthDb.ts",
"test:e2e": "env-cmd -f .env.test.local playwright test",
"test:e2e": "playwright test",
"test": "cd npm && npm run test"
},
"dependencies": {
@ -96,4 +96,4 @@
"engines": {
"node": ">=14.18.1 <=18.x"
}
}
}

View File

@ -107,6 +107,9 @@ const Login = ({ csrfToken, tenant, product }: InferGetServerSidePropsType<typeo
container: 'mt-2',
button: 'btn-outline btn-block btn',
}}
innerProps={{
button: { 'data-testid': 'sso-login-button' },
}}
/>
</div>
</div>

View File

@ -17,10 +17,13 @@ const config: PlaywrightTestConfig = {
// Run your local dev server before starting the tests:
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: {
command: process.env.CI ? 'npm run start' : 'npm run postgres',
command: process.env.CI ? 'npm run start' : 'npm run build && npm run start',
port: 5225,
timeout: 60 * 1000,
reuseExistingServer: !process.env.CI,
env: {
NODE_ENV: 'test',
},
},
use: {

View File

@ -46,9 +46,9 @@ export interface LoginProps {
*/
classNames?: { container?: string; button?: string; input?: string; label?: string };
innerProps?: {
input?: InputHTMLAttributes<HTMLInputElement>;
button?: ButtonHTMLAttributes<HTMLButtonElement>;
label?: LabelHTMLAttributes<HTMLLabelElement>;
container?: HTMLAttributes<HTMLDivElement>;
input?: InputHTMLAttributes<HTMLInputElement> & { 'data-testid'?: string };
button?: ButtonHTMLAttributes<HTMLButtonElement> & { 'data-testid'?: string };
label?: LabelHTMLAttributes<HTMLLabelElement> & { 'data-testid'?: string };
container?: HTMLAttributes<HTMLDivElement> & { 'data-testid'?: string };
};
}