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

Merge pull request #3299 from logto-io/gao-improve-ui-test

test: improve ui test code
This commit is contained in:
Gao Sun 2023-03-08 10:44:48 +08:00 committed by GitHub
commit e5b055f173
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 106 deletions

View file

@ -51,7 +51,7 @@ const Tenants = ({ data, onAdd }: Props) => {
</a> </a>
))} ))}
<h3>Create a tenant</h3> <h3>Create a tenant</h3>
<Button title={<DangerousRaw>Create New</DangerousRaw>} onClick={createTenant} /> <Button title={<DangerousRaw>Create</DangerousRaw>} onClick={createTenant} />
</div> </div>
); );
} }

View file

@ -30,8 +30,9 @@
"@silverhand/eslint-config": "2.0.1", "@silverhand/eslint-config": "2.0.1",
"@silverhand/essentials": "2.4.0", "@silverhand/essentials": "2.4.0",
"@silverhand/ts-config": "2.0.3", "@silverhand/ts-config": "2.0.3",
"@types/expect-puppeteer": "^5.0.3",
"@types/jest": "^29.1.2", "@types/jest": "^29.1.2",
"@types/jest-environment-puppeteer": "^5.0.2", "@types/jest-environment-puppeteer": "^5.0.3",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"eslint": "^8.34.0", "eslint": "^8.34.0",

View file

@ -10,64 +10,78 @@ const appendPathname = (pathname: string, baseUrl: URL) =>
* NOTE: This test suite assumes test cases will run sequentially (which is Jest default). * NOTE: This test suite assumes test cases will run sequentially (which is Jest default).
* Parallel execution will lead to errors. * Parallel execution will lead to errors.
*/ */
// Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md
// for convenient expect methods
describe('smoke testing for cloud', () => { describe('smoke testing for cloud', () => {
const consoleUsername = 'admin'; const consoleUsername = 'admin';
const consolePassword = generatePassword(); const consolePassword = generatePassword();
const logtoCloudUrl = new URL(logtoCloudUrlString); const logtoCloudUrl = new URL(logtoCloudUrlString);
const adminTenantUrl = new URL(logtoConsoleUrl); // In dev mode, the console URL is actually for admin tenant const adminTenantUrl = new URL(logtoConsoleUrl); // In dev mode, the console URL is actually for admin tenant
it('opens with app element and navigates to sign-in page', async () => { it('can open with app element and navigate to register page', async () => {
const navigation = page.waitForNavigation({ waitUntil: 'networkidle0' });
await page.goto(logtoCloudUrl.href); await page.goto(logtoCloudUrl.href);
await navigation; await page.waitForNavigation({ waitUntil: 'networkidle0' });
await expect(page.waitForSelector('#app')).resolves.not.toBeNull(); await expect(page).toMatchElement('#app');
expect(page.url()).toBe(appendPathname('/register', adminTenantUrl).href); expect(page.url()).toBe(appendPathname('/register', adminTenantUrl).href);
}); });
it('registers the first admin account', async () => { it('can register the first admin account', async () => {
const createAccountButton = await page.waitForSelector('button'); await expect(page).toClick('button', { text: 'Create account' });
expect(createAccountButton).not.toBeNull();
const usernameField = await page.waitForSelector('input[name=identifier]'); await expect(page).toFill('input[name=identifier]', consoleUsername);
const submitButton = await page.waitForSelector('button[name=submit]'); await expect(page).toClick('button[name=submit]');
await usernameField.type(consoleUsername);
const navigateToCreatePassword = page.waitForNavigation({ waitUntil: 'networkidle0' });
await submitButton.click();
await navigateToCreatePassword;
await page.waitForNavigation({ waitUntil: 'networkidle0' });
expect(page.url()).toBe(appendPathname('/register/password', adminTenantUrl).href); expect(page.url()).toBe(appendPathname('/register/password', adminTenantUrl).href);
const passwordField = await page.waitForSelector('input[name=newPassword]'); await expect(page).toFillForm('form', {
const confirmPasswordField = await page.waitForSelector('input[name=confirmPassword]'); newPassword: consolePassword,
const saveButton = await page.waitForSelector('button[name=submit]'); confirmPassword: consolePassword,
await passwordField.type(consolePassword); });
await confirmPasswordField.type(consolePassword); await expect(page).toClick('button[name=submit]');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
const navigateToCloud = page.waitForNavigation({ waitUntil: 'networkidle0' });
await saveButton.click();
await navigateToCloud;
expect(page.url()).toBe(logtoCloudUrl.href); expect(page.url()).toBe(logtoCloudUrl.href);
}); });
it('shows a tenant-select page with two tenants', async () => { it('shows a tenant-select page with two tenants', async () => {
const tenantsWrapper = await page.waitForSelector('div[class$=wrapper]'); const tenantsWrapper = await page.waitForSelector('div[class$=wrapper]');
const tenants = await tenantsWrapper.$$('a');
const hrefs = await Promise.all(
tenants.map(async (element) => {
const value = await element.getProperty('href');
return value.jsonValue(); await expect(tenantsWrapper).toMatchElement('a:nth-of-type(1)', { text: 'default' });
}) await expect(tenantsWrapper).toMatchElement('a:nth-of-type(2)', { text: 'admin' });
); });
expect( it('can create another tenant', async () => {
['default', 'admin'].every((tenantId) => await expect(page).toClick('button', { text: 'Create' });
hrefs.some((href) => String(href).endsWith('/' + tenantId))
) await page.waitForTimeout(1000);
const tenants = await page.$$('div[class$=wrapper] > a');
expect(tenants.length).toBe(3);
});
it('can enter the tenant just created', async () => {
const button = await page.waitForSelector('div[class$=wrapper] > a:last-of-type');
const tenantId = await button.evaluate((element) => element.textContent);
await button.click();
// Wait for our beautiful logto to show up
await page.waitForSelector('div[class$=topbar] > svg[viewbox][class$=logo]');
expect(page.url()).toBe(new URL(`/${tenantId ?? ''}/onboard/welcome`, logtoCloudUrl).href);
});
it('can sign out of admin console', async () => {
await expect(page).toClick('div[class$=topbar] > div[class$=container]');
// Try awaiting for 500ms before clicking sign-out button
await page.waitForTimeout(500);
await expect(page).toClick(
'.ReactModalPortal div[class$=dropdownContainer] div[class$=dropdownItem]:last-child'
); );
await page.waitForNavigation({ waitUntil: 'networkidle0' });
expect(page.url()).toBe(new URL('sign-in', logtoConsoleUrl).href);
}); });
}); });

View file

@ -1,102 +1,87 @@
import { logtoConsoleUrl } from '#src/constants.js'; import path from 'path';
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
import { generatePassword } from '#src/utils.js'; import { generatePassword } from '#src/utils.js';
const appendPathname = (pathname: string, baseUrl: URL) =>
new URL(path.join(baseUrl.pathname, pathname), baseUrl);
const logtoConsoleUrl = new URL(logtoConsoleUrlString);
/** /**
* NOTE: This test suite assumes test cases will run sequentially (which is Jest default). * NOTE: This test suite assumes test cases will run sequentially (which is Jest default).
* Parallel execution will lead to errors. * Parallel execution will lead to errors.
*/ */
// Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md
// for convenient expect methods
describe('smoke testing', () => { describe('smoke testing', () => {
const consoleUsername = 'admin'; const consoleUsername = 'admin';
const consolePassword = generatePassword(); const consolePassword = generatePassword();
it('opens with app element and navigates to welcome page', async () => { it('can open with app element and navigate to welcome page', async () => {
const navigation = page.waitForNavigation({ waitUntil: 'networkidle0' }); await page.goto(logtoConsoleUrl.href);
await page.goto(logtoConsoleUrl); await page.waitForNavigation({ waitUntil: 'networkidle0' });
await navigation;
await expect(page.waitForSelector('#app')).resolves.not.toBeNull(); await expect(page).toMatchElement('#app');
expect(page.url()).toBe(new URL('console/welcome', logtoConsoleUrl).href); expect(page.url()).toBe(new URL('console/welcome', logtoConsoleUrl).href);
}); });
it('registers a new admin account and automatically signs in', async () => { it('can register a new admin account and automatically sign in', async () => {
const createAccountButton = await page.waitForSelector('button'); await expect(page).toClick('button', { text: 'Create account' });
expect(createAccountButton).not.toBeNull();
const navigateToRegister = page.waitForNavigation({ waitUntil: 'networkidle0' });
await createAccountButton.click();
await navigateToRegister;
await page.waitForNavigation({ waitUntil: 'networkidle0' });
expect(page.url()).toBe(new URL('register', logtoConsoleUrl).href); expect(page.url()).toBe(new URL('register', logtoConsoleUrl).href);
const usernameField = await page.waitForSelector('input[name=identifier]'); await expect(page).toFill('input[name=identifier]', consoleUsername);
const submitButton = await page.waitForSelector('button[name=submit]'); await expect(page).toClick('button[name=submit]');
await usernameField.type(consoleUsername); await page.waitForNavigation({ waitUntil: 'networkidle0' });
expect(page.url()).toBe(appendPathname('/register/password', logtoConsoleUrl).href);
const navigateToSignIn = page.waitForNavigation({ waitUntil: 'networkidle0' }); await expect(page).toFillForm('form', {
await submitButton.click(); newPassword: consolePassword,
await navigateToSignIn; confirmPassword: consolePassword,
});
expect(page.url()).toBe(new URL('register/password', logtoConsoleUrl).href); await expect(page).toClick('button[name=submit]');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
const passwordField = await page.waitForSelector('input[name=newPassword]');
const confirmPasswordField = await page.waitForSelector('input[name=confirmPassword]');
const saveButton = await page.waitForSelector('button[name=submit]');
await passwordField.type(consolePassword);
await confirmPasswordField.type(consolePassword);
const navigateToGetStarted = page.waitForNavigation({ waitUntil: 'networkidle0' });
await saveButton.click();
await navigateToGetStarted;
expect(page.url()).toBe(new URL('console/get-started', logtoConsoleUrl).href); expect(page.url()).toBe(new URL('console/get-started', logtoConsoleUrl).href);
}); });
it('signs out of admin console', async () => { it('can sign out of admin console', async () => {
const userElement = await page.waitForSelector('div[class$=topbar] > div[class$=container]'); await expect(page).toClick('div[class$=topbar] > div[class$=container]');
await userElement.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);
const signOutButton = await page.waitForSelector( await expect(page).toClick(
'.ReactModalPortal div[class$=dropdownContainer] div[class$=dropdownItem]:last-child' '.ReactModalPortal div[class$=dropdownContainer] div[class$=dropdownItem]:last-child'
); );
const navigation = page.waitForNavigation({ waitUntil: 'networkidle0' }); await page.waitForNavigation({ waitUntil: 'networkidle0' });
await signOutButton.click();
await navigation;
expect(page.url()).toBe(new URL('sign-in', logtoConsoleUrl).href); expect(page.url()).toBe(new URL('sign-in', logtoConsoleUrl).href);
}); });
it('signs in to admin console', async () => { it('can sign in to admin console', async () => {
const usernameField = await page.waitForSelector('input[name=identifier]'); await expect(page).toFillForm('form', {
const passwordField = await page.waitForSelector('input[name=password]'); identifier: consoleUsername,
const submitButton = await page.waitForSelector('button[name=submit]'); password: consolePassword,
});
await usernameField.type(consoleUsername); await expect(page).toClick('button[name=submit]');
await passwordField.type(consolePassword); await page.waitForNavigation({ waitUntil: 'networkidle0' });
const navigation = page.waitForNavigation({ waitUntil: 'networkidle0' });
await submitButton.click();
await navigation;
expect(page.url()).toBe(new URL('console/get-started', logtoConsoleUrl).href); expect(page.url()).toBe(new URL('console/get-started', logtoConsoleUrl).href);
const userElement = await page.waitForSelector('div[class$=topbar] > div:last-child'); await expect(page).toClick('div[class$=topbar] > div:last-child');
await userElement.click();
const userMenu = await page.waitForSelector('.ReactModalPortal div[class$=dropdownContainer]'); const userMenu = await page.waitForSelector('.ReactModalPortal div[class$=dropdownContainer]');
const usernameString = await userMenu.$eval( await expect(userMenu).toMatchElement('div[class$=nameWrapper] > div[class$=name]', {
'div[class$=nameWrapper] > div[class$=name]', text: consoleUsername,
(element) => element.textContent });
);
expect(usernameString).toBe(consoleUsername);
}); });
it('renders SVG correctly with viewbox property', async () => { it('renders SVG correctly with viewbox property', async () => {
const logoSvg = await page.waitForSelector('div[class$=topbar] > svg[viewbox]'); await page.waitForSelector('div[class$=topbar] > svg[viewbox][class$=logo]');
expect(logoSvg).not.toBeNull();
}); });
}); });

View file

@ -2,7 +2,7 @@ const welcome = {
title: 'Welcome to Admin Console', title: 'Welcome to Admin Console',
description: description:
'Admin console is a web app to manage Logto without coding requirements. Lets first create an account. With this account, you can manage Logto by yourself or on behalf of your company.', 'Admin console is a web app to manage Logto without coding requirements. Lets first create an account. With this account, you can manage Logto by yourself or on behalf of your company.',
create_account: 'Create Account', create_account: 'Create account',
}; };
export default welcome; export default welcome;

View file

@ -551,8 +551,9 @@ importers:
'@silverhand/eslint-config': 2.0.1 '@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.4.0 '@silverhand/essentials': 2.4.0
'@silverhand/ts-config': 2.0.3 '@silverhand/ts-config': 2.0.3
'@types/expect-puppeteer': ^5.0.3
'@types/jest': ^29.1.2 '@types/jest': ^29.1.2
'@types/jest-environment-puppeteer': ^5.0.2 '@types/jest-environment-puppeteer': ^5.0.3
'@types/node': ^18.11.18 '@types/node': ^18.11.18
'@withtyped/server': ^0.8.0 '@withtyped/server': ^0.8.0
dotenv: ^16.0.0 dotenv: ^16.0.0
@ -579,8 +580,9 @@ importers:
'@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy '@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy
'@silverhand/essentials': 2.4.0 '@silverhand/essentials': 2.4.0
'@silverhand/ts-config': 2.0.3_typescript@4.9.4 '@silverhand/ts-config': 2.0.3_typescript@4.9.4
'@types/expect-puppeteer': 5.0.3
'@types/jest': 29.1.2 '@types/jest': 29.1.2
'@types/jest-environment-puppeteer': 5.0.2 '@types/jest-environment-puppeteer': 5.0.3
'@types/node': 18.11.18 '@types/node': 18.11.18
dotenv: 16.0.0 dotenv: 16.0.0
eslint: 8.34.0 eslint: 8.34.0
@ -4135,6 +4137,13 @@ packages:
'@types/node': 18.11.18 '@types/node': 18.11.18
dev: true dev: true
/@types/expect-puppeteer/5.0.3:
resolution: {integrity: sha512-NIqATm95VmFbc2s9v1L3yj9ZS9/rCrtptSgBsvW8mcw2KFpLFQqXPyEbo0Vju1eiBieI38jRGWgpbVuUKfQVoQ==}
dependencies:
'@types/jest': 29.1.2
'@types/puppeteer': 5.4.6
dev: true
/@types/express-serve-static-core/4.17.26: /@types/express-serve-static-core/4.17.26:
resolution: {integrity: sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==} resolution: {integrity: sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==}
dependencies: dependencies:
@ -4224,8 +4233,8 @@ packages:
'@types/istanbul-lib-report': 3.0.0 '@types/istanbul-lib-report': 3.0.0
dev: true dev: true
/@types/jest-environment-puppeteer/5.0.2: /@types/jest-environment-puppeteer/5.0.3:
resolution: {integrity: sha512-YCdegQnDou6aIK8wqygDDctHgJZtlXd03fI9Bgbqkdu66EFKnImmt7auiR9OkxkSSiqS9smn0joX2pGpVs7ErA==} resolution: {integrity: sha512-vWGfeb+0TOPZy7+VscKURWzE5lzYjclSWLxtjVpDAYcjUv8arAS1av06xK3mpgeNCDVx7XvavD8Elq1a4w9wIA==}
dependencies: dependencies:
'@jest/types': 27.5.1 '@jest/types': 27.5.1
'@types/puppeteer': 5.4.6 '@types/puppeteer': 5.4.6
@ -4532,10 +4541,6 @@ packages:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: true dev: true
/@types/yargs-parser/20.2.1:
resolution: {integrity: sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==}
dev: true
/@types/yargs-parser/21.0.0: /@types/yargs-parser/21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: true dev: true
@ -4543,7 +4548,7 @@ packages:
/@types/yargs/16.0.4: /@types/yargs/16.0.4:
resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==} resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==}
dependencies: dependencies:
'@types/yargs-parser': 20.2.1 '@types/yargs-parser': 21.0.0
dev: true dev: true
/@types/yargs/17.0.13: /@types/yargs/17.0.13:
@ -9634,7 +9639,7 @@ packages:
'@jest/types': 27.5.1 '@jest/types': 27.5.1
'@types/node': 18.11.18 '@types/node': 18.11.18
chalk: 4.1.2 chalk: 4.1.2
ci-info: 3.5.0 ci-info: 3.8.0
graceful-fs: 4.2.10 graceful-fs: 4.2.10
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true