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:
parent
7ea5b94178
commit
c4826556d9
7 changed files with 226 additions and 41 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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\/.+$/);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue