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

test: add ui tests for applications (#4407)

* test: add ui tests for applications

* refactor(test): simplify application ui tests
This commit is contained in:
Xiao Yijun 2023-08-31 14:31:03 +08:00 committed by GitHub
parent 4f5881304e
commit 2b39964fd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 461 additions and 1 deletions

View file

@ -0,0 +1,60 @@
import { ApplicationType } from '@logto/schemas';
export type ApplicationCase = {
framework: string;
name: string;
description: string;
guideFilename: string;
sample: {
repo: string;
path: string;
};
redirectUri: string;
postSignOutRedirectUri: string;
};
export const initialApp: ApplicationCase = {
framework: 'Next.js',
name: 'Next.js App',
description: 'This is a Next.js app',
guideFilename: 'web-next',
sample: {
repo: 'js',
path: 'packages/next-sample',
},
redirectUri: 'https://my.test.app/sign-in',
postSignOutRedirectUri: 'https://my.test.app/sign-out',
};
export const testApp: ApplicationCase = {
framework: 'Go',
name: 'Go App',
description: 'This is a Go app',
guideFilename: 'web-go',
sample: {
repo: 'go',
path: 'gin-sample',
},
redirectUri: 'https://my.test.app/sign-in',
postSignOutRedirectUri: 'https://my.test.app/sign-out',
};
export const frameworkGroupLabels = [
'Popular and for you',
'Traditional web app',
'Single page app',
'Native',
'Machine-to-machine',
] as const;
export type ApplicationMetadata = {
type: ApplicationType;
name: string;
description: string;
};
export const applicationTypesMetadata = Object.entries(ApplicationType).map(([key, value]) => ({
type: value,
name: `${key} app`,
description: `This is a ${key} app`,
})) satisfies ApplicationMetadata[];

View file

@ -0,0 +1,123 @@
import { appendPath } from '@silverhand/essentials';
import { type Page } from 'puppeteer';
import {
expectModalWithTitle,
expectToClickDetailsPageOption,
expectToClickModalAction,
expectToOpenNewPage,
waitForToast,
} from '#src/ui-helpers/index.js';
import { expectNavigation } from '#src/utils.js';
import { frameworkGroupLabels, type ApplicationCase } from './constants.js';
export const expectFrameworksInGroup = async (page: Page, groupSelector: string) => {
/* eslint-disable no-await-in-loop */
for (const groupLabel of frameworkGroupLabels) {
const frameGroup = await expect(page).toMatchElement(groupSelector, {
text: groupLabel,
});
const frameworks = await frameGroup.$$('div[class$=grid] div[class*=card]');
expect(frameworks.length).toBeGreaterThan(0);
}
/* eslint-enable no-await-in-loop */
};
export const expectToClickFramework = async (page: Page, framework: string) => {
const frameworkCard = await expect(page).toMatchElement(
'div[class*=card]:has(div[class$=header] div[class$=name])',
{
text: framework,
}
);
await expect(frameworkCard).toClick('button span', { text: 'Start Building' });
};
export const expectFrameworkExists = async (page: Page, framework: string) => {
await expect(page).toMatchElement('div[class*=card]:has(div[class$=header] div[class$=name])', {
text: framework,
});
};
export const expectToProceedCreationFrom = async (
page: Page,
{ name, description }: ApplicationCase
) => {
// Expect the creation form to be open
await expectModalWithTitle(page, 'Create Application');
await expect(page).toFillForm('.ReactModalPortal form', {
name,
description,
});
await expectToClickModalAction(page, 'Create Application');
await waitForToast(page, { text: 'Application created successfully.' });
};
export const expectToProceedSdkGuide = async (
page: Page,
{ guideFilename, sample, redirectUri, postSignOutRedirectUri }: ApplicationCase,
skipFillForm = false
) => {
await expectModalWithTitle(page, 'Start with SDK and guides');
expect(page.url()).toContain(`/guide/${guideFilename}`);
await expect(page).toClick('.ReactModalPortal aside[class$=sample] a span', {
text: 'Check out sample',
});
await expectToOpenNewPage(
browser,
appendPath(new URL('https://github.com/logto-io'), sample.repo, 'tree/HEAD', sample.path).href
);
if (!skipFillForm) {
const redirectUriFieldWrapper = await expect(page).toMatchElement(
'div[class$=wrapper]:has(>div[class$=field]>div[class$=headline]>div[class$=title])',
{ text: 'Redirect URI' }
);
await expect(redirectUriFieldWrapper).toFill('input', redirectUri);
await expect(redirectUriFieldWrapper).toClick('button span', { text: 'Save' });
await waitForToast(page, { text: 'Saved' });
const postSignOutRedirectUriWrapper = await expect(page).toMatchElement(
'div[class$=wrapper]:has(>div[class$=field]>div[class$=headline]>div[class$=title])',
{ text: 'Post Sign-out Redirect URI' }
);
await expect(postSignOutRedirectUriWrapper).toFill('input', postSignOutRedirectUri);
await expect(postSignOutRedirectUriWrapper).toClick('button span', { text: 'Save' });
await waitForToast(page, { text: 'Saved' });
}
// Finish guide
await expect(page).toClick('.ReactModalPortal nav[class$=actionBar] button span', {
text: 'Finish and done',
});
};
export const expectToProceedAppDeletion = async (page: Page, appName: string) => {
// Delete the application
await expectToClickDetailsPageOption(page, 'Delete');
// Confirm deletion
await expectModalWithTitle(page, 'Reminder');
await expect(page).toFill('.ReactModalPortal div[class$=deleteConfirm] input', appName);
await expectNavigation(expectToClickModalAction(page, 'Delete'));
await waitForToast(page, {
text: `Application ${appName} has been successfully deleted`,
});
};

View file

@ -0,0 +1,268 @@
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
import {
expectConfirmModalAndAct,
expectModalWithTitle,
expectToClickModalAction,
expectToDiscardChanges,
expectToOpenNewPage,
expectToSaveChanges,
expectUnsavedChangesAlert,
goToAdminConsole,
waitForToast,
} from '#src/ui-helpers/index.js';
import { expectNavigation, appendPathname } from '#src/utils.js';
import {
type ApplicationMetadata,
applicationTypesMetadata,
initialApp,
testApp,
} from './constants.js';
import {
expectFrameworkExists,
expectToClickFramework,
expectToProceedCreationFrom,
expectToProceedSdkGuide,
expectToProceedAppDeletion,
expectFrameworksInGroup,
} from './helpers.js';
await page.setViewport({ width: 1920, height: 1080 });
describe('applications', () => {
const logtoConsoleUrl = new URL(logtoConsoleUrlString);
beforeAll(async () => {
await goToAdminConsole();
});
it('navigate to applications page', async () => {
await expectNavigation(
page.goto(appendPathname('/console/applications', logtoConsoleUrl).href)
);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);
});
it('the table placeholder should be rendered correctly', async () => {
await expect(page).toMatchElement(
'div[class$=guideLibraryContainer] div[class$=titleEllipsis]',
{ text: 'Select a framework or tutorial', timeout: 2000 }
);
await expectFrameworksInGroup(page, 'div[class$=guideGroup]:has(>label)');
});
it('create the initial application from the table placeholder', async () => {
await expectToClickFramework(page, initialApp.framework);
await expectToProceedCreationFrom(page, initialApp);
await expectToProceedSdkGuide(page, initialApp, true);
// Details page
await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', {
text: initialApp.name,
});
// Back to application list page
await expectNavigation(
expect(page).toClick('div[class$=main] a[class$=backLink]', {
text: 'Back to Applications',
})
);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);
/**
* Note:
* Reload the page to refresh new application data by the SWR,
* since test operations is so quick and the SWR is not updated
*/
await page.reload();
await expect(page).toMatchElement('table div[class$=item] a[class$=title]', {
text: initialApp.name,
});
});
it('can open the logto github repo issue page when click on the framework not found button', async () => {
await expect(page).toClick('div[class$=main] div[class$=headline] button span', {
text: 'Create Application',
});
await expectModalWithTitle(page, 'Start with SDK and guides');
// Click request sdk button
await expect(page).toClick(
'.ReactModalPortal div[class$=header] button[class$=requestSdkButton]'
);
await expectToOpenNewPage(browser, 'https://github.com/logto-io/logto/issues');
// Return to the application list page
await expectNavigation(
expect(page).toClick('.ReactModalPortal div[class$=header] button:has(svg[class$=closeIcon])')
);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);
});
it('can create an application by framework from the app creation modal and modify its data', async () => {
await expect(page).toClick('div[class$=main] div[class$=headline] button span', {
text: 'Create Application',
});
await expectModalWithTitle(page, 'Start with SDK and guides');
await expectFrameworksInGroup(page, '.ReactModalPortal div[class$=guideGroup]:has(>label)');
// Expect the framework contains on the page
await expectFrameworkExists(page, testApp.framework);
// Filter
await expect(page).toFill('div[class$=searchInput] input', testApp.framework);
// Expect the framework exists after filtering
await expectFrameworkExists(page, testApp.framework);
await expectToClickFramework(page, testApp.framework);
await expectToProceedCreationFrom(page, testApp);
await expectToProceedSdkGuide(page, testApp);
// Expect on the details page
await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', {
text: testApp.name,
});
// Check guide
await expect(page).toClick('div[class$=header] button span', { text: 'Check Guide' });
// Wait for the guide drawer to be ready
await page.waitForTimeout(500);
// Close guide
await expect(page).toClick(
'.ReactModalPortal div[class$=drawerContainer] div[class$=header] button:last-of-type'
);
// Wait for the guide drawer to disappear
await page.waitForSelector('.ReactModalPortal div[class$=drawerContainer]', { hidden: true });
// Update application data
await expect(page).toFillForm('form', {
description: `(New): ${testApp.description}`,
});
await expectUnsavedChangesAlert(page);
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
const redirectUriFiled = await expect(page).toMatchElement(
'div[class$=field]:has(>div[class$=headline]>div[class$=title]',
{ text: 'Redirect URIs' }
);
// Add and remove redirect uri
await expect(redirectUriFiled).toClick('div[class$=multilineInput]>button>span', {
text: 'Add Another',
});
// Wait for the new redirect uri field
await page.waitForSelector(
'div:has(>div[class$=deletableInput]):last-of-type button:has(svg[class$=minusIcon])'
);
await expect(redirectUriFiled).toFill(
'div:has(>div[class$=deletableInput]):last-of-type input',
`${testApp.redirectUri}/v2`
);
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
// Click delete button
await expect(redirectUriFiled).toClick(
'div:has(>div[class$=deletableInput]):last-of-type button:has(svg[class$=minusIcon])'
);
await expectConfirmModalAndAct(page, { title: 'Reminder', actionText: 'Delete' });
await expectToSaveChanges(page);
await waitForToast(page, { text: 'Saved' });
// Wait for the redirect uri field to be updated
await page.waitForTimeout(500);
// Remove Redirect Uri
await expect(page).toFill(`input[value="${testApp.redirectUri}"]`, '');
await expectToSaveChanges(page);
// Expect error
await expect(page).toMatchElement(
'div[class$=field] div[class$=multilineInput] div[class$=error]',
{
text: 'You must enter at least one redirect URI',
}
);
await expectToDiscardChanges(page);
await expectToProceedAppDeletion(page, testApp.name);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);
});
it.each(applicationTypesMetadata)(
'can create and modify a(n) $type application without framework',
async (app: ApplicationMetadata) => {
await expect(page).toClick('div[class$=main] div[class$=headline] button span', {
text: 'Create Application',
});
await expect(page).toClick('.ReactModalPortal nav[class$=actionBar] button span', {
text: 'Create app without framework',
});
await expectModalWithTitle(page, 'Create Application');
await expect(page).toClick(`div[class*=radio][role=radio]:has(input[value=${app.type}])`);
await expect(page).toFillForm('.ReactModalPortal form', {
name: app.name,
description: app.description,
});
await expectToClickModalAction(page, 'Create Application');
await waitForToast(page, { text: 'Application created successfully.' });
await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', {
text: app.name,
});
await expectToProceedAppDeletion(page, app.name);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);
}
);
it('delete the initial application', async () => {
await expect(page).toClick('table tbody tr td div[class$=item] a[class$=title]', {
text: initialApp.name,
});
await expect(page).toMatchElement('div[class$=main] div[class$=header] div[class$=name]', {
text: initialApp.name,
});
await expectToProceedAppDeletion(page, initialApp.name);
expect(page.url()).toBe(new URL('/console/applications', logtoConsoleUrl).href);
});
});

View file

@ -1,4 +1,4 @@
import { type Page } from 'puppeteer';
import { type Browser, type Page } from 'puppeteer';
import {
consolePassword,
@ -120,3 +120,12 @@ export const expectToClickNavTab = async (page: Page, tab: string) => {
text: tab,
});
};
export const expectToOpenNewPage = async (browser: Browser, url: string) => {
const target = await browser.waitForTarget((target) => target.url() === url);
const newPage = await target.page();
expect(newPage).toBeTruthy();
await newPage?.close();
};