0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix(console): fix multi tenancy flow and sync ui-test

This commit is contained in:
Darcy Ye 2023-06-27 15:35:05 +08:00
parent ad6dde7f18
commit 51fb943986
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
5 changed files with 199 additions and 71 deletions

View file

@ -1,15 +1,12 @@
import { useHandleSignInCallback } from '@logto/react'; import { useHandleSignInCallback } from '@logto/react';
import { conditionalString } from '@silverhand/essentials';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import AppLoading from '@/components/AppLoading'; import AppLoading from '@/components/AppLoading';
import { isCloud } from '@/consts/env';
import { getUserTenantId } from '@/consts/tenants';
function Callback() { function Callback() {
const navigate = useNavigate(); const navigate = useNavigate();
useHandleSignInCallback(() => { useHandleSignInCallback(() => {
navigate('/' + conditionalString(isCloud && getUserTenantId()), { replace: true }); navigate('/', { replace: true });
}); });
return <AppLoading />; return <AppLoading />;

View file

@ -27,7 +27,8 @@ const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => {
function TenantBasicSettings() { function TenantBasicSettings() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useCloudApi(); const api = useCloudApi();
const { currentTenant, currentTenantId, updateTenant, removeTenant } = useContext(TenantsContext); const { currentTenant, currentTenantId, updateTenant, removeTenant, navigateTenant } =
useContext(TenantsContext);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [isDeletionModalOpen, setIsDeletionModalOpen] = useState(false); const [isDeletionModalOpen, setIsDeletionModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
@ -89,6 +90,7 @@ function TenantBasicSettings() {
await api.delete(`/api/tenants/:tenantId`, { params: { tenantId: currentTenantId } }); await api.delete(`/api/tenants/:tenantId`, { params: { tenantId: currentTenantId } });
setIsDeletionModalOpen(false); setIsDeletionModalOpen(false);
removeTenant(currentTenantId); removeTenant(currentTenantId);
navigateTenant('');
} catch (error: unknown) { } catch (error: unknown) {
setError( setError(
error instanceof Error error instanceof Error

View file

@ -0,0 +1,94 @@
import { type Page } from 'puppeteer';
export const onboardingWelcome = async (page: Page) => {
// Select the project type option
await page.click('div[role=radio]:has(input[name=project][value=personal])');
// Select the deployment type option
await page.click('div[role=radio]:has(input[name=deploymentType][value=open-source])');
// Click the next button
await page.click('div[class$=actions] button:first-child');
};
export const onboardingUserSurvey = async (page: Page) => {
// Wait for the sie config to load
await page.waitForTimeout(1000);
// Select the first reason option
await page.click('div[role=button][class$=item]');
// Click the next button
await expect(page).toClick('div[class$=actions] button', { text: 'Next' });
};
export const onboardingSieConfig = async (page: Page) => {
// Wait for the sie config to load
await page.waitForTimeout(1000);
// Select username as the identifier
await page.click('div[role=radio]:has(input[name=identifier][value=username])');
// Click the finish button
await page.click('div[class$=continueActions] button:last-child');
};
export const onboardingFinish = async (page: Page) => {
// Wait for the sie config to load
await page.waitForTimeout(1000);
// Click the enter ac button
await page.click('div[class$=content] >button');
// Wait for the admin console to load
await page.waitForNavigation({ waitUntil: 'networkidle0' });
};
export const openTenantDropdown = async (page: Page) => {
// Click 'current tenant card' locates in topbar
const currentTenantCard = await page.waitForSelector(
'div[class$=topbar] > div[class$=currentTenantCard][role=button]:has(div[class$=name])'
);
await currentTenantCard?.click();
};
export const openCreateTenantModal = async (page: Page) => {
const createTenantButton = await page.waitForSelector(
'div[class$=ReactModalPortal] div[class$=dropdownContainer] div[class$=dropdown] button[class$=createTenantButton]:has(div)'
);
await createTenantButton?.click();
};
export const fillAndCreateTenant = async (page: Page, tenantName: string) => {
// Create tenant with name 'new-tenant' and tag 'production'
await page.waitForTimeout(500);
await page.waitForSelector(
'div[class$=ReactModalPortal] div[class*=card][class$=medium] input[type=text][name=name]'
);
await page.waitForSelector(
'div[class$=ReactModalPortal] div[class*=radioGroup][class$=small] div[class*=radio][class$=small][role=radio] > div[class$=content]:has(input[value=production])'
);
await page.type(
'div[class$=ReactModalPortal] div[class*=card][class$=medium] input[type=text][name=name]',
tenantName
);
await page.click(
'div[class$=ReactModalPortal] div[class*=radioGroup][class$=small] div[class*=radio][class$=small][role=radio] > div[class$=content]:has(input[value=production])'
);
// Click create button
await page.waitForTimeout(500);
await page.click(
'div[class$=ReactModalPortal] div[class*=card][class$=medium] div[class$=footer] button[type=submit]'
);
};
export const createNewTenant = async (page: Page, tenantName: string) => {
await page.waitForTimeout(500);
await openTenantDropdown(page);
await page.waitForTimeout(500);
await openCreateTenantModal(page);
await fillAndCreateTenant(page, tenantName);
};

View file

@ -5,6 +5,17 @@ import { setDefaultOptions } from 'expect-puppeteer';
import { logtoCloudUrl as logtoCloudUrlString, logtoConsoleUrl } from '#src/constants.js'; import { logtoCloudUrl as logtoCloudUrlString, logtoConsoleUrl } from '#src/constants.js';
import { generatePassword } from '#src/utils.js'; import { generatePassword } from '#src/utils.js';
import {
onboardingWelcome,
onboardingUserSurvey,
onboardingSieConfig,
onboardingFinish,
createNewTenant,
fillAndCreateTenant,
openTenantDropdown,
openCreateTenantModal,
} from './operations.js';
await page.setViewport({ width: 1280, height: 720 }); await page.setViewport({ width: 1280, height: 720 });
setDefaultOptions({ timeout: 5000 }); setDefaultOptions({ timeout: 5000 });
@ -51,16 +62,7 @@ describe('smoke testing for cloud', () => {
}); });
it('can complete the onboarding welcome process and enter the user survey page', async () => { it('can complete the onboarding welcome process and enter the user survey page', async () => {
// Select the project type option await onboardingWelcome(page);
await expect(page).toClick('div[role=radio]:has(input[name=project][value=personal])');
// Select the deployment type option
await expect(page).toClick(
'div[role=radio]:has(input[name=deploymentType][value=open-source])'
);
// Click the next button
await expect(page).toClick('div[class$=actions] button:first-child');
// Wait for the next page to load // Wait for the next page to load
await expect(page).toMatchElement('div[class$=content] div[class$=title]', { await expect(page).toMatchElement('div[class$=content] div[class$=title]', {
@ -71,11 +73,7 @@ describe('smoke testing for cloud', () => {
}); });
it('can complete the onboarding user survey process and enter the sie page', async () => { it('can complete the onboarding user survey process and enter the sie page', async () => {
// Select the first reason option await onboardingUserSurvey(page);
await expect(page).toClick('div[role=button][class$=item]');
// Click the next button
await expect(page).toClick('div[class$=actions] button', { text: 'Next' });
// Wait for the next page to load // Wait for the next page to load
await expect(page).toMatchElement('div[class$=config] div[class$=title]', { await expect(page).toMatchElement('div[class$=config] div[class$=title]', {
@ -86,14 +84,7 @@ describe('smoke testing for cloud', () => {
}); });
it('can complete the sie configuration process and enter the congrats page', async () => { it('can complete the sie configuration process and enter the congrats page', async () => {
// Wait for the sie config to load await onboardingSieConfig(page);
await page.waitForTimeout(1000);
// Select username as the identifier
await expect(page).toClick('div[role=radio]:has(input[name=identifier][value=username])');
// Click the finish button
await expect(page).toClick('div[class$=continueActions] button:last-child');
// Wait for the next page to load // Wait for the next page to load
await expect(page).toMatchElement('div[class$=content] div[class$=title]', { await expect(page).toMatchElement('div[class$=content] div[class$=title]', {
@ -104,11 +95,7 @@ describe('smoke testing for cloud', () => {
}); });
it('can complete the onboarding process and enter the admin console', async () => { it('can complete the onboarding process and enter the admin console', async () => {
// Click the enter ac button await onboardingFinish(page);
await expect(page).toClick('div[class$=content] >button');
// Wait for the admin console to load
await page.waitForNavigation({ waitUntil: 'networkidle0' });
const mainContent = await page.waitForSelector('div[class$=main]:has(div[class$=title])'); const mainContent = await page.waitForSelector('div[class$=main]:has(div[class$=title])');
await expect(mainContent).toMatchElement('div[class$=title]', { await expect(mainContent).toMatchElement('div[class$=title]', {
text: 'Something to explore to help you succeed', text: 'Something to explore to help you succeed',
@ -118,41 +105,8 @@ describe('smoke testing for cloud', () => {
}); });
it('can create a new tenant using tenant dropdown', async () => { it('can create a new tenant using tenant dropdown', async () => {
// Click 'current tenant card' locates in topbar await page.waitForTimeout(2000);
const currentTenantCard = await page.waitForSelector( await createNewTenant(page, createTenantName);
'div[class$=topbar] > div[class$=currentTenantCard][role=button]:has(div[class$=name])'
);
await expect(currentTenantCard).toMatchElement('div[class$=name]', { text: 'My Project' });
await currentTenantCard.click();
await page.waitForTimeout(500);
const createTenantButton = await page.waitForSelector(
'div[class$=ReactModalPortal] div[class$=dropdownContainer] > div[class$=dropdown] > button[class$=createTenantButton]:has(div)'
);
await expect(createTenantButton).toMatchElement('div', { text: 'Create tenant' });
await createTenantButton.click();
// Create tenant with name 'new-tenant' and tag 'production'
await page.waitForTimeout(500);
await page.waitForSelector(
'div[class$=ReactModalPortal] div[class*=card][class$=medium] input[type=text][name=name]'
);
await page.waitForSelector(
'div[class$=ReactModalPortal] div[class*=radioGroup][class$=small] div[class*=radio][class$=small][role=radio] > div[class$=content]:has(input[value=production])'
);
await expect(page).toFill(
'div[class$=ReactModalPortal] div[class*=card][class$=medium] input[type=text][name=name]',
createTenantName
);
await expect(page).toClick(
'div[class$=ReactModalPortal] div[class*=radioGroup][class$=small] div[class*=radio][class$=small][role=radio] > div[class$=content]:has(input[value=production])'
);
// Click create button
await page.waitForTimeout(500);
await expect(page).toClick(
'div[class$=ReactModalPortal] div[class*=card][class$=medium] div[class$=footer] button[type=submit]'
);
expect(new URL(page.url()).pathname.endsWith(`/get-started`)).toBeTruthy(); expect(new URL(page.url()).pathname.endsWith(`/get-started`)).toBeTruthy();
}); });
@ -170,7 +124,7 @@ describe('smoke testing for cloud', () => {
it('can sign out of admin console', async () => { it('can sign out of admin console', async () => {
const userInfoButton = await page.waitForSelector('div[class$=topbar] > div[class$=container]'); const userInfoButton = await page.waitForSelector('div[class$=topbar] > div[class$=container]');
await userInfoButton.click(); await userInfoButton?.click();
// Try awaiting for 500ms before clicking sign-out button // Try awaiting for 500ms before clicking sign-out button
await page.waitForTimeout(500); await page.waitForTimeout(500);
@ -178,7 +132,7 @@ describe('smoke testing for cloud', () => {
const signOutButton = await page.waitForSelector( const signOutButton = await page.waitForSelector(
'div[class$=ReactModalPortal] div[class$=dropdownContainer] div[class$=dropdownItem]:last-child' 'div[class$=ReactModalPortal] div[class$=dropdownContainer] div[class$=dropdownItem]:last-child'
); );
await signOutButton.click(); await signOutButton?.click();
await page.waitForNavigation({ waitUntil: 'networkidle0' }); await page.waitForNavigation({ waitUntil: 'networkidle0' });
@ -208,4 +162,84 @@ describe('smoke testing for cloud', () => {
expect(page.url().startsWith(logtoCloudUrl.origin)).toBeTruthy(); expect(page.url().startsWith(logtoCloudUrl.origin)).toBeTruthy();
expect(page.url().endsWith('/onboarding/welcome')).toBeTruthy(); expect(page.url().endsWith('/onboarding/welcome')).toBeTruthy();
}); });
it('can complete onboarding process with new account', async () => {
await onboardingWelcome(page);
await onboardingUserSurvey(page);
await onboardingSieConfig(page);
await onboardingFinish(page);
await page.waitForTimeout(1000);
expect(new URL(page.url()).pathname.endsWith('/get-started')).toBeTruthy();
});
it('go to tenant settings and delete current tenant', async () => {
await page.waitForTimeout(2000);
const tenantSettingButton = await page.waitForSelector(
'div[class$=content] > div[class$=sidebar] a[class$=row][href$=tenant-settings] > div[class$=title]'
);
await tenantSettingButton?.click();
const deleteButton = await page.waitForSelector(
'div[class$=main] form[class$=container] div[class$=deletionButtonContainer] button[class$=medium][type=button]'
);
await deleteButton?.click();
const textInput = await page.waitForSelector(
'div[class$=ReactModalPortal] div[class$=container] input[type=text]'
);
await textInput?.type('My Project');
const deleteConfirmButton = await page.waitForSelector(
'div[class$=ReactModalPortal] div[class$=footer] > button:last-child'
);
await deleteConfirmButton?.click();
await page.waitForTimeout(2000);
const placeholderTitle = await page.waitForSelector(
'div[class$=pageContainer] div[class$=placeholder]:has(div[class$=title])'
);
await expect(placeholderTitle).toMatchElement('div[class$=title]', {
text: 'You havent created a tenant yet',
});
});
it('can create tenant from landing page', async () => {
const createTenantButton = await page.waitForSelector(
'div[class$=pageContainer] div[class$=placeholder] button[class$=button][type=button]'
);
await createTenantButton?.click();
await fillAndCreateTenant(page, 'tenant1');
await page.waitForTimeout(5000);
expect(new URL(page.url()).pathname.endsWith('/get-started')).toBeTruthy();
});
it('can create two more tenant for new account', async () => {
await createNewTenant(page, 'tenant2');
await page.waitForTimeout(5000);
await createNewTenant(page, 'tenant3');
await page.waitForTimeout(5000);
expect(new URL(page.url()).pathname.endsWith('/get-started')).toBeTruthy();
});
it('can not open create tenant modal when reach the limit', async () => {
await page.waitForTimeout(2000);
await openTenantDropdown(page);
await openCreateTenantModal(page);
await page.waitForTimeout(500);
const pageTitle = await page.waitForSelector(
'div[class$=main] > div[class$=container] > div[class$=header]:has(div[class$=title])'
);
await expect(pageTitle).toMatchElement('div[class$=title]', {
text: 'Something to explore to help you succeed',
});
const createTenantModalTitleSelector =
'div[class$=ReactModalPortal] div[class$=iconAndTitle] > div[class*=container][class$=large]:has(div[class*=title][class$=titleEllipsis])';
await expect(
page.waitForSelector(createTenantModalTitleSelector, { timeout: 3000 })
).rejects.toThrow(); // Throws error if selector is not found.
}, 20_000);
}); });

View file

@ -9,7 +9,8 @@
"#src/*": [ "#src/*": [
"src/*" "src/*"
] ]
} },
"types": ["jest", "jest-puppeteer"],
}, },
"include": ["src"] "include": ["src"]
} }