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}
>
<FormField isRequired title="general.name">
<TextInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
placeholder={t('organizations.organization_name_placeholder')}
error={Boolean(errors.name)}
{...register('name', { required: true })}
/>
</FormField>
<FormField title="general.description">
<TextInput
error={Boolean(errors.description)}
placeholder={t('organizations.organization_description_placeholder')}
{...register('description')}
/>
</FormField>
<form>
<FormField isRequired title="general.name">
<TextInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
placeholder={t('organizations.organization_name_placeholder')}
error={Boolean(errors.name)}
{...register('name', { required: true })}
/>
</FormField>
<FormField title="general.description">
<TextInput
error={Boolean(errors.description)}
placeholder={t('organizations.organization_description_placeholder')}
{...register('description')}
/>
</FormField>
</form>
</ModalLayout>
</ReactModal>
);

View file

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

View file

@ -22,6 +22,6 @@ export const signUpIdentifiers = {
none: [],
};
export const consoleUsername = 'admin';
export const consolePassword = 'some_random_password_123';
export const consoleUsername = 'svhd';
export const consolePassword = 'silverhandasd_1';
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;
};
export type ConsoleTitle = 'Sign-in experience';
export type ConsoleTitle = 'Sign-in experience' | 'Organizations';
export default class ExpectConsole extends ExpectPage {
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.
*/
async gotoPage(pathname: string, title: ConsoleTitle) {
await this.navigateTo(this.buildUrl(path.join(this.options.tenantId, pathname)));
await expect(this.page).toMatchElement(
[dcls('main'), dcls('container'), dcls('cardTitle')].join(' '),
[dcls('main'), dcls('container'), dcls('title')].join(' '),
{ 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.
*

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;
}
/**
* 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.
*
@ -64,6 +75,13 @@ export default class ExpectPage {
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.
*
@ -116,9 +134,14 @@ export default class ExpectPage {
/**
* 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);
}