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

chore(test): init org ui tests

This commit is contained in:
Gao Sun 2023-10-26 18:03:16 +08:00
parent 7ea5b94178
commit c4826556d9
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
7 changed files with 226 additions and 41 deletions

View file

@ -60,22 +60,24 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
} }
onClose={onClose} onClose={onClose}
> >
<FormField isRequired title="general.name"> <form>
<TextInput <FormField isRequired title="general.name">
// eslint-disable-next-line jsx-a11y/no-autofocus <TextInput
autoFocus // eslint-disable-next-line jsx-a11y/no-autofocus
placeholder={t('organizations.organization_name_placeholder')} autoFocus
error={Boolean(errors.name)} placeholder={t('organizations.organization_name_placeholder')}
{...register('name', { required: true })} error={Boolean(errors.name)}
/> {...register('name', { required: true })}
</FormField> />
<FormField title="general.description"> </FormField>
<TextInput <FormField title="general.description">
error={Boolean(errors.description)} <TextInput
placeholder={t('organizations.organization_description_placeholder')} error={Boolean(errors.description)}
{...register('description')} placeholder={t('organizations.organization_description_placeholder')}
/> {...register('description')}
</FormField> />
</FormField>
</form>
</ModalLayout> </ModalLayout>
</ReactModal> </ReactModal>
); );

View file

@ -78,25 +78,27 @@ function PermissionModal({ isOpen, editData, onClose }: Props) {
} }
onClose={onClose} onClose={onClose}
> >
<FormField isRequired title="general.name"> <form>
<TextInput <FormField isRequired title="general.name">
// eslint-disable-next-line jsx-a11y/no-autofocus <TextInput
autoFocus // eslint-disable-next-line jsx-a11y/no-autofocus
placeholder="read:appointment" autoFocus
error={Boolean(errors.name)} placeholder="read:appointment"
disabled={Boolean(editData)} error={Boolean(errors.name)}
{...register('name', { required: true })} disabled={Boolean(editData)}
/> {...register('name', { required: true })}
</FormField> />
<FormField title="general.description"> </FormField>
<TextInput <FormField title="general.description">
// eslint-disable-next-line jsx-a11y/no-autofocus <TextInput
autoFocus={Boolean(editData)} // eslint-disable-next-line jsx-a11y/no-autofocus
placeholder={t('organizations.create_permission_placeholder')} autoFocus={Boolean(editData)}
error={Boolean(errors.description)} placeholder={t('organizations.create_permission_placeholder')}
{...register('description')} error={Boolean(errors.description)}
/> {...register('description')}
</FormField> />
</FormField>
</form>
</ModalLayout> </ModalLayout>
</ReactModal> </ReactModal>
); );

View file

@ -22,6 +22,6 @@ export const signUpIdentifiers = {
none: [], none: [],
}; };
export const consoleUsername = 'admin'; export const consoleUsername = 'svhd';
export const consolePassword = 'some_random_password_123'; export const consolePassword = 'silverhandasd_1';
export const mockSocialAuthPageUrl = 'http://mock.social.com'; export const mockSocialAuthPageUrl = 'http://mock.social.com';

View file

@ -0,0 +1,70 @@
import ExpectOrganizations from '#src/ui-helpers/expect-organizations.js';
import { cls, dcls, generateTestName } from '#src/utils.js';
const expectOrg = new ExpectOrganizations(await browser.newPage());
describe('organizations: create, edit, and delete organization', () => {
it('navigates to organizations page', async () => {
await expectOrg.start();
await expectOrg.gotoPage('/organizations', 'Organizations');
await expectOrg.toExpectTabs('Organizations', 'Settings');
});
it('should be able to see the table', async () => {
await expectOrg.toExpectTableHeaders('Name', 'Organization ID', 'Members');
});
it('should be able to create a new organization', async () => {
await expectOrg.toCreateOrganization('Test organization');
});
it('should be able to edit the created organization', async () => {
await expectOrg.toExpectTabs('Settings', 'Members');
const nameUpdated = 'Test organization updated';
await expectOrg.toFillForm({
name: nameUpdated,
description: 'Test organization description',
});
await expectOrg.toSaveChanges();
await expectOrg.toMatchElement([dcls('header'), dcls('metadata'), dcls('name')].join(' '), {
text: nameUpdated,
});
});
it('should be able to delete the created organization', async () => {
// Open the dropdown menu
await expectOrg.toClick(
[dcls('header'), `button${cls('withIcon')}`].join(' '),
undefined,
false
);
await expectOrg.toClick(`${dcls('danger')}[role=menuitem]`, 'Delete organization', false);
await expectOrg.toExpectModal('Reminder');
await expectOrg.toClick(['.ReactModalPortal', `button${cls('danger')}`].join(' '), 'Delete');
expectOrg.toMatchUrl(/\/organizations$/);
});
});
describe('organizations: search organization', () => {
const [testName1, testName2] = [generateTestName(), generateTestName()];
it('creates two organizations', async () => {
await expectOrg.start();
await expectOrg.toCreateOrganization(testName1);
await expectOrg.toCreateOrganization(testName2);
await expectOrg.gotoPage('/organizations', 'Organizations');
});
it('should be able to search organization', async () => {
await expectOrg.toFill([dcls('search'), 'input'].join(' '), testName1);
await expectOrg.toClickButton('Search', false);
await expectOrg.toExpectTableCell(testName1);
// This will work as expected since we can ensure the search result is ready from the previous step
await expect(expectOrg.page).not.toMatchElement([`table`, 'tbody', 'tr', 'td'].join(' '), {
text: new RegExp(testName2, 'i'),
visible: true,
});
});
});

View file

@ -19,7 +19,7 @@ type ExpectConsoleOptions = {
tenantId?: string; tenantId?: string;
}; };
export type ConsoleTitle = 'Sign-in experience'; export type ConsoleTitle = 'Sign-in experience' | 'Organizations';
export default class ExpectConsole extends ExpectPage { export default class ExpectConsole extends ExpectPage {
readonly options: Required<ExpectConsoleOptions>; readonly options: Required<ExpectConsoleOptions>;
@ -48,17 +48,82 @@ export default class ExpectConsole extends ExpectPage {
} }
} }
/**
* Alias for `expect(page).toMatchElement(...)`.
*
* @see {@link jest.Matchers.toMatchElement}
*/
async toMatchElement(...args: Parameters<jest.Matchers<unknown>['toMatchElement']>) {
return expect(this.page).toMatchElement(...args);
}
/** /**
* Navigate to a specific page in the Console. * Navigate to a specific page in the Console.
*/ */
async gotoPage(pathname: string, title: ConsoleTitle) { async gotoPage(pathname: string, title: ConsoleTitle) {
await this.navigateTo(this.buildUrl(path.join(this.options.tenantId, pathname))); await this.navigateTo(this.buildUrl(path.join(this.options.tenantId, pathname)));
await expect(this.page).toMatchElement( await expect(this.page).toMatchElement(
[dcls('main'), dcls('container'), dcls('cardTitle')].join(' '), [dcls('main'), dcls('container'), dcls('title')].join(' '),
{ text: title } { text: title }
); );
} }
/**
* Expect navigation tabs with the given names to be rendered in the Console.
*
* @param tabNames The names of the tabs to expect, case-insensitive.
*/
async toExpectTabs(...tabNames: string[]) {
await Promise.all(
tabNames.map(async (tabName) => {
return expect(this.page).toMatchElement(
['nav', dcls('item'), dcls('link'), 'a'].join(' '),
{ text: new RegExp(tabName, 'i'), visible: true }
);
})
);
}
/**
* Expect table headers with the given names to be rendered in the Console.
*
* @param headers The names of the table headers to expect, case-insensitive.
*/
async toExpectTableHeaders(...headers: string[]) {
await Promise.all(
headers.map(async (header) => {
return expect(this.page).toMatchElement(
[`table${cls('headerTable')}`, 'thead', 'tr', 'th'].join(' '),
{ text: new RegExp(header, 'i'), visible: true }
);
})
);
}
/**
* Expect a table cell with the given text to be rendered in the Console.
*
* @param text The text to expect, case-insensitive.
*/
async toExpectTableCell(text: string) {
await expect(this.page).toMatchElement([`table`, 'tbody', 'tr', 'td'].join(' '), {
text: new RegExp(text, 'i'),
visible: true,
});
}
/**
* Expect a modal to appear with the given title.
*
* @param title The title of the modal to expect, case-insensitive.
*/
async toExpectModal(title: string) {
await expect(this.page).toMatchElement(
['div.ReactModalPortal', dcls('header'), dcls('title')].join(' '),
{ text: new RegExp(title, 'i'), visible: true }
);
}
/** /**
* Expect card components to be rendered in the Console. * Expect card components to be rendered in the Console.
* *

View file

@ -0,0 +1,23 @@
import { cls } from '#src/utils.js';
import ExpectConsole from './expect-console.js';
export default class ExpectOrganizations extends ExpectConsole {
/**
* Go to the organizations page and create a new organization, then assert that the URL matches
* the organization detail page.
*
* @param name The name of the organization to create.
*/
async toCreateOrganization(name: string) {
await this.gotoPage('/organizations', 'Organizations');
await this.toClickButton('Create organization');
await this.toExpectModal('Create organization');
await this.toFillForm({
name,
});
await this.toClick(['.ReactModalPortal', `button${cls('primary')}`].join(' '), 'Create', false);
this.toMatchUrl(/\/organizations\/.+$/);
}
}

View file

@ -41,6 +41,17 @@ export default class ExpectPage {
return shouldNavigate ? expectNavigation(clicked, this.page) : clicked; return shouldNavigate ? expectNavigation(clicked, this.page) : clicked;
} }
/**
* Shortcut for {@link ExpectPage.toClick} with the selector `button` and the given text.
*
* @param text The text to match.
* @param shouldNavigate Whether the click should trigger a navigation. Defaults to `true`.
* @see {@link ExpectPage.toClick}
*/
async toClickButton(text: string, shouldNavigate = true) {
return this.toClick('button', text, shouldNavigate);
}
/** /**
* Click on the `<button type="submit">` element on the page. * Click on the `<button type="submit">` element on the page.
* *
@ -64,6 +75,13 @@ export default class ExpectPage {
return shouldNavigate ? expectNavigation(submitted, this.page) : submitted; return shouldNavigate ? expectNavigation(submitted, this.page) : submitted;
} }
/**
* Alias for {@link jest.Matchers['toFill']}.
*/
async toFill(...args: Parameters<jest.Matchers<unknown>['toFill']>) {
return expect(this.page).toFill(...args);
}
/** /**
* Fill an `<input>` with the given name with the given value and optionally submit the form. * Fill an `<input>` with the given name with the given value and optionally submit the form.
* *
@ -116,9 +134,14 @@ export default class ExpectPage {
/** /**
* Expect the page's URL to match the given URL. * Expect the page's URL to match the given URL.
* *
* @param url The URL to match. * @param url The URL to match, or a regular expression to match the URL against.
*/ */
toMatchUrl(url: URL | string) { toMatchUrl(url: URL | string | RegExp) {
if (url instanceof RegExp) {
expect(this.page.url()).toMatch(url);
return;
}
expect(this.page.url()).toBe(typeof url === 'string' ? url : url.href); expect(this.page.url()).toBe(typeof url === 'string' ? url : url.href);
} }