mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
test(test): add tests for console user management (#3863)
* test(test): add tests for console user management * chore: remove unused dependency * chore: remove testing screenshots
This commit is contained in:
parent
db2bc1a5a6
commit
f1730db70b
10 changed files with 321 additions and 23 deletions
|
@ -7,6 +7,7 @@ const config = {
|
||||||
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
||||||
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
|
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
|
||||||
},
|
},
|
||||||
|
testSequencer: './ui-test-sequencer.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -21,9 +21,9 @@
|
||||||
"start": "pnpm test"
|
"start": "pnpm test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/test-sequencer": "^29.5.0",
|
||||||
"@jest/types": "^29.1.2",
|
"@jest/types": "^29.1.2",
|
||||||
"@logto/connector-kit": "workspace:^1.1.0",
|
"@logto/connector-kit": "workspace:^1.1.0",
|
||||||
"@logto/language-kit": "workspace:^1.0.0",
|
|
||||||
"@logto/js": "^2.0.1",
|
"@logto/js": "^2.0.1",
|
||||||
"@logto/node": "^2.0.0",
|
"@logto/node": "^2.0.0",
|
||||||
"@logto/schemas": "workspace:^1.1.0",
|
"@logto/schemas": "workspace:^1.1.0",
|
||||||
|
|
|
@ -20,3 +20,6 @@ export const signUpIdentifiers = {
|
||||||
emailOrSms: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
emailOrSms: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||||
none: [],
|
none: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const consoleUsername = 'admin';
|
||||||
|
export const consolePassword = 'abcd1234';
|
||||||
|
|
|
@ -57,6 +57,9 @@ describe('admin console user management', () => {
|
||||||
|
|
||||||
const newUserData = {
|
const newUserData = {
|
||||||
name: 'new name',
|
name: 'new name',
|
||||||
|
primaryEmail: generateEmail(),
|
||||||
|
primaryPhone: generatePhone(),
|
||||||
|
username: generateUsername(),
|
||||||
avatar: 'https://new.avatar.com/avatar.png',
|
avatar: 'https://new.avatar.com/avatar.png',
|
||||||
customData: {
|
customData: {
|
||||||
level: 1,
|
level: 1,
|
||||||
|
@ -69,10 +72,17 @@ describe('admin console user management', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when update userinfo with conflict identifiers', async () => {
|
it('should fail when update userinfo with conflict identifiers', async () => {
|
||||||
const user = await createUserByAdmin();
|
const [username, email, phone] = [generateUsername(), generateEmail(), generatePhone()];
|
||||||
|
await createUserByAdmin(username, undefined, email, phone);
|
||||||
const anotherUser = await createUserByAdmin();
|
const anotherUser = await createUserByAdmin();
|
||||||
|
|
||||||
await expect(updateUser(user.id, { username: anotherUser.username })).rejects.toMatchObject(
|
await expect(updateUser(anotherUser.id, { username })).rejects.toMatchObject(
|
||||||
|
createResponseWithCode(422)
|
||||||
|
);
|
||||||
|
await expect(updateUser(anotherUser.id, { primaryEmail: email })).rejects.toMatchObject(
|
||||||
|
createResponseWithCode(422)
|
||||||
|
);
|
||||||
|
await expect(updateUser(anotherUser.id, { primaryPhone: phone })).rejects.toMatchObject(
|
||||||
createResponseWithCode(422)
|
createResponseWithCode(422)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
import { setDefaultOptions } from 'expect-puppeteer';
|
import { setDefaultOptions } from 'expect-puppeteer';
|
||||||
|
|
||||||
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
|
import {
|
||||||
import { generatePassword } from '#src/utils.js';
|
consolePassword,
|
||||||
|
consoleUsername,
|
||||||
setDefaultOptions({ timeout: 2000 });
|
logtoConsoleUrl as logtoConsoleUrlString,
|
||||||
|
} from '#src/constants.js';
|
||||||
const appendPathname = (pathname: string, baseUrl: URL) =>
|
import { appendPathname } from '#src/utils.js';
|
||||||
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).
|
||||||
|
@ -18,9 +13,9 @@ const logtoConsoleUrl = new URL(logtoConsoleUrlString);
|
||||||
*/
|
*/
|
||||||
// Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md
|
// Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md
|
||||||
// for convenient expect methods
|
// for convenient expect methods
|
||||||
describe('smoke testing', () => {
|
describe('smoke testing for console admin account creation and sign-in', () => {
|
||||||
const consoleUsername = 'admin';
|
setDefaultOptions({ timeout: 2000 });
|
||||||
const consolePassword = generatePassword();
|
const logtoConsoleUrl = new URL(logtoConsoleUrlString);
|
||||||
|
|
||||||
it('can open with app element and navigate to welcome page', async () => {
|
it('can open with app element and navigate to welcome page', async () => {
|
||||||
await page.goto(logtoConsoleUrl.href);
|
await page.goto(logtoConsoleUrl.href);
|
||||||
|
@ -68,6 +63,8 @@ describe('smoke testing', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can sign in to admin console', async () => {
|
it('can sign in to admin console', async () => {
|
||||||
|
await page.goto(logtoConsoleUrl.href);
|
||||||
|
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||||
await expect(page).toFillForm('form', {
|
await expect(page).toFillForm('form', {
|
||||||
identifier: consoleUsername,
|
identifier: consoleUsername,
|
||||||
password: consolePassword,
|
password: consolePassword,
|
||||||
|
@ -83,6 +80,8 @@ describe('smoke testing', () => {
|
||||||
await expect(userMenu).toMatchElement('div[class$=nameWrapper] > div[class$=name]', {
|
await expect(userMenu).toMatchElement('div[class$=nameWrapper] > div[class$=name]', {
|
||||||
text: consoleUsername,
|
text: consoleUsername,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await expect(page).toClick('div[class^=ReactModal__Overlay]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders SVG correctly with viewbox property', async () => {
|
it('renders SVG correctly with viewbox property', async () => {
|
236
packages/integration-tests/src/tests/ui/user-management.test.ts
Normal file
236
packages/integration-tests/src/tests/ui/user-management.test.ts
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
import {
|
||||||
|
consolePassword,
|
||||||
|
consoleUsername,
|
||||||
|
logtoConsoleUrl as logtoConsoleUrlString,
|
||||||
|
} from '#src/constants.js';
|
||||||
|
import {
|
||||||
|
appendPathname,
|
||||||
|
formatPhoneNumberToInternational,
|
||||||
|
generateEmail,
|
||||||
|
generateName,
|
||||||
|
generatePhone,
|
||||||
|
generateUsername,
|
||||||
|
} from '#src/utils.js';
|
||||||
|
|
||||||
|
await page.setViewport({ width: 1280, height: 720 });
|
||||||
|
|
||||||
|
describe('user management', () => {
|
||||||
|
const logtoConsoleUrl = new URL(logtoConsoleUrlString);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await page.goto(logtoConsoleUrl.href);
|
||||||
|
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
if (page.url() === new URL('sign-in', logtoConsoleUrl).href) {
|
||||||
|
await expect(page).toFillForm('form', {
|
||||||
|
identifier: consoleUsername,
|
||||||
|
password: consolePassword,
|
||||||
|
});
|
||||||
|
await expect(page).toClick('button[name=submit]');
|
||||||
|
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to user management page on clicking sidebar menu', async () => {
|
||||||
|
await page.goto(appendPathname('/console/users', logtoConsoleUrl).href);
|
||||||
|
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
await expect(page).toMatchElement(
|
||||||
|
'div[class$=main] div[class$=headline] div[class$=titleEllipsis]',
|
||||||
|
{
|
||||||
|
text: 'User Management',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create a new user', async () => {
|
||||||
|
await expect(page).toClick('div[class$=main] div[class$=headline] > button');
|
||||||
|
await expect(page).toFillForm('form', {
|
||||||
|
primaryEmail: 'jdoe@gmail.com',
|
||||||
|
primaryPhone: '+18105555555',
|
||||||
|
username: 'johndoe',
|
||||||
|
});
|
||||||
|
await expect(page).toClick('button[type=submit]');
|
||||||
|
await page.waitForSelector('div[class$=infoLine');
|
||||||
|
await expect(page).toMatchElement(
|
||||||
|
'.ReactModalPortal div[class$=header] div[class$=titleEllipsis]',
|
||||||
|
{
|
||||||
|
text: 'This user has been successfully created',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Go to user details page
|
||||||
|
await expect(page).toClick('div.ReactModalPortal div[class$=footer] button:first-of-type');
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: 'jdoe@gmail.com',
|
||||||
|
});
|
||||||
|
const userId = await page.$eval(
|
||||||
|
'div[class$=main] div[class$=metadata] div[class$=row] div:first-of-type',
|
||||||
|
(element) => element.textContent
|
||||||
|
);
|
||||||
|
if (userId) {
|
||||||
|
expect(page.url()).toBe(new URL(`console/users/${userId}/settings`, logtoConsoleUrl).href);
|
||||||
|
}
|
||||||
|
const email = await page.$eval('form input[name=primaryEmail]', (element) =>
|
||||||
|
element instanceof HTMLInputElement ? element.value : null
|
||||||
|
);
|
||||||
|
const phone = await page.$eval('form input[name=primaryPhone]', (element) =>
|
||||||
|
element instanceof HTMLInputElement ? element.value : null
|
||||||
|
);
|
||||||
|
const username = await page.$eval('form input[name=username]', (element) =>
|
||||||
|
element instanceof HTMLInputElement ? element.value : null
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(email).toBe('jdoe@gmail.com');
|
||||||
|
expect(phone).toBe('+1 810 555 5555');
|
||||||
|
expect(username).toBe('johndoe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails to create user if no identifier is provided', async () => {
|
||||||
|
await page.goto(appendPathname('/console/users', logtoConsoleUrl).href);
|
||||||
|
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
await expect(page).toClick('div[class$=main] div[class$=headline] > button');
|
||||||
|
await expect(page).toClick('button[type=submit]');
|
||||||
|
await expect(page).toMatchElement('.ReactModalPortal div[class$=error]', {
|
||||||
|
text: 'You must provide at least one identifier to create a user.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails to create user if any of the identifiers are existed', async () => {
|
||||||
|
await page.goto(appendPathname('/console/users', logtoConsoleUrl).href);
|
||||||
|
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
// Conflicted email
|
||||||
|
await expect(page).toClick('div[class$=main] div[class$=headline] > button');
|
||||||
|
await expect(page).toFillForm('form', { primaryEmail: 'jdoe@gmail.com' });
|
||||||
|
await expect(page).toClick('button[type=submit]');
|
||||||
|
await expect(page).toMatchElement('div[class$=error] span[class$=message]', {
|
||||||
|
text: 'This email is associated with an existing account.',
|
||||||
|
});
|
||||||
|
await expect(page).toClick('.ReactModalPortal div[class$=header] button');
|
||||||
|
|
||||||
|
// Conflicted phone number
|
||||||
|
await expect(page).toClick('div[class$=main] div[class$=headline] > button');
|
||||||
|
await expect(page).toFillForm('form', { primaryPhone: '+1 810 555 5555' });
|
||||||
|
await expect(page).toClick('button[type=submit]');
|
||||||
|
await expect(page).toMatchElement('div[class$=error] span[class$=message]', {
|
||||||
|
text: 'This phone number is associated with an existing account.',
|
||||||
|
});
|
||||||
|
await expect(page).toClick('.ReactModalPortal div[class$=header] button');
|
||||||
|
|
||||||
|
// Conflicted username
|
||||||
|
await expect(page).toClick('div[class$=main] div[class$=headline] > button');
|
||||||
|
await expect(page).toFillForm('form', { username: 'johndoe' });
|
||||||
|
await expect(page).toClick('button[type=submit]');
|
||||||
|
await expect(page).toMatchElement('div[class$=error] span[class$=message]', {
|
||||||
|
text: 'This username is already in use.',
|
||||||
|
});
|
||||||
|
await expect(page).toClick('.ReactModalPortal div[class$=header] button');
|
||||||
|
// Wait for 5 seconds for the error toasts to dismiss
|
||||||
|
await page.waitForTimeout(4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can update user details', async () => {
|
||||||
|
// Create a new user and navigates to the user details page
|
||||||
|
const username = generateUsername();
|
||||||
|
await expect(page).toClick('div[class$=main] div[class$=headline] > button');
|
||||||
|
|
||||||
|
await expect(page).toFillForm('form', { username });
|
||||||
|
await expect(page).toClick('button[type=submit]');
|
||||||
|
await page.waitForSelector('div[class$=infoLine');
|
||||||
|
|
||||||
|
// Go to the user details page
|
||||||
|
await expect(page).toClick('div.ReactModalPortal div[class$=footer] button:nth-of-type(1)');
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: username,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newUsername = generateUsername();
|
||||||
|
const newEmail = generateEmail();
|
||||||
|
const newPhone = generatePhone(true);
|
||||||
|
const newFullName = generateName();
|
||||||
|
await expect(page).toFillForm('form', {
|
||||||
|
primaryEmail: newEmail,
|
||||||
|
primaryPhone: newPhone,
|
||||||
|
username: newUsername,
|
||||||
|
name: newFullName,
|
||||||
|
});
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
const successToastHandle = await page.waitForSelector('div[class$=success]');
|
||||||
|
await expect(successToastHandle).toMatchElement('span[class$=message]', {
|
||||||
|
text: 'Saved!',
|
||||||
|
});
|
||||||
|
// Top userinfo card shows the updated user full name as the title
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: newFullName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page).toFillForm('form', { name: '' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
// After removing full name, top userinfo card shows the email as the title
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: newEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(page).toFillForm('form', { primaryEmail: '' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
// After removing email, top userinfo card shows the phone number as the title
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: formatPhoneNumberToInternational(newPhone),
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await expect(page).toFillForm('form', { primaryPhone: '' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
// After removing phone number, top userinfo card shows the username as the title
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: newUsername,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await expect(page).toFillForm('form', { username: '' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
// After removing all identifiers, saving the form will pop up a confirm dialog
|
||||||
|
await expect(page).toMatchElement('.ReactModalPortal div[class$=medium] div[class$=content]', {
|
||||||
|
text: 'User needs to have at least one of the sign-in identifiers (username, email, phone number or social) to sign in. Are you sure you want to continue?',
|
||||||
|
});
|
||||||
|
await expect(page).toClick('div.ReactModalPortal div[class$=footer] button:nth-of-type(2)');
|
||||||
|
// After all identifiers, top userinfo card shows 'Unnamed' as the title
|
||||||
|
await expect(page).toMatchElement('div[class$=main] div[class$=metadata] div[class$=title]', {
|
||||||
|
text: 'Unnamed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails to update if any of the identifiers are conflicted with existing users', async () => {
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForSelector('form');
|
||||||
|
// Conflicted email
|
||||||
|
await expect(page).toFillForm('form', { primaryEmail: 'jdoe@gmail.com' });
|
||||||
|
await page.screenshot({ path: 'user-conflict-email.png' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
await expect(page).toMatchElement('div[class$=error] span[class$=message]', {
|
||||||
|
text: 'This email is associated with an existing account.',
|
||||||
|
});
|
||||||
|
// Discard changes
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(1)');
|
||||||
|
|
||||||
|
// Conflicted phone number
|
||||||
|
await expect(page).toFillForm('form', { primaryPhone: '+1 810 555 5555' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
await expect(page).toMatchElement('div[class$=error] span[class$=message]', {
|
||||||
|
text: 'This phone number is associated with an existing account.',
|
||||||
|
});
|
||||||
|
// Discard changes
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(1)');
|
||||||
|
|
||||||
|
// Conflicted username
|
||||||
|
await expect(page).toFillForm('form', { username: 'johndoe' });
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(2)');
|
||||||
|
await expect(page).toMatchElement('div[class$=error] span[class$=message]', {
|
||||||
|
text: 'This username is already in use.',
|
||||||
|
});
|
||||||
|
// Discard changes
|
||||||
|
await expect(page).toClick('form div[class$=actionBar] button:nth-of-type(1)');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
@ -13,12 +14,39 @@ export const generateEmail = () => `${crypto.randomUUID().toLowerCase()}@logto.i
|
||||||
export const generateScopeName = () => `sc:${crypto.randomUUID()}`;
|
export const generateScopeName = () => `sc:${crypto.randomUUID()}`;
|
||||||
export const generateRoleName = () => `role_${crypto.randomUUID()}`;
|
export const generateRoleName = () => `role_${crypto.randomUUID()}`;
|
||||||
|
|
||||||
export const generatePhone = () => {
|
export const generatePhone = (isE164?: boolean) => {
|
||||||
const array = new Uint32Array(1);
|
const plus = isE164 ? '+' : '';
|
||||||
|
const countryAndAreaCode = '1310'; // California, US
|
||||||
|
const validCentralOfficeCodes = [
|
||||||
|
'205',
|
||||||
|
'208',
|
||||||
|
'215',
|
||||||
|
'216',
|
||||||
|
'220',
|
||||||
|
'228',
|
||||||
|
'229',
|
||||||
|
'230',
|
||||||
|
'231',
|
||||||
|
'232',
|
||||||
|
];
|
||||||
|
const centralOfficeCode =
|
||||||
|
validCentralOfficeCodes[Math.floor(Math.random() * validCentralOfficeCodes.length)] ?? '205';
|
||||||
|
const phoneNumber = Math.floor(Math.random() * 10_000)
|
||||||
|
.toString()
|
||||||
|
.padStart(4, '0');
|
||||||
|
|
||||||
return crypto.getRandomValues(array).join('');
|
return plus + countryAndAreaCode + centralOfficeCode + phoneNumber;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatPhoneNumberToInternational = (phoneNumber: string) =>
|
||||||
|
phoneNumber.slice(0, 2) +
|
||||||
|
' ' +
|
||||||
|
phoneNumber.slice(2, 5) +
|
||||||
|
' ' +
|
||||||
|
phoneNumber.slice(5, 8) +
|
||||||
|
' ' +
|
||||||
|
phoneNumber.slice(8);
|
||||||
|
|
||||||
export const waitFor = async (ms: number) =>
|
export const waitFor = async (ms: number) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
|
@ -32,3 +60,6 @@ export const getAccessTokenPayload = (accessToken: string): Record<string, unkno
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
return JSON.parse(payload) as Record<string, unknown>;
|
return JSON.parse(payload) as Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const appendPathname = (pathname: string, baseUrl: URL) =>
|
||||||
|
new URL(path.join(baseUrl.pathname, pathname), baseUrl);
|
||||||
|
|
18
packages/integration-tests/ui-test-sequencer.js
Normal file
18
packages/integration-tests/ui-test-sequencer.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const Sequencer = require('@jest/test-sequencer').default;
|
||||||
|
|
||||||
|
const bootstrapTestSuitePathSuffix = '/tests/ui/bootstrap.test.js';
|
||||||
|
|
||||||
|
class CustomSequencer extends Sequencer {
|
||||||
|
sort(tests) {
|
||||||
|
const bootstrap = tests.find(({ path }) => path.includes(bootstrapTestSuitePathSuffix));
|
||||||
|
return [
|
||||||
|
bootstrap,
|
||||||
|
...tests.filter(({ path }) => !path.includes(bootstrapTestSuitePathSuffix)),
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomSequencer;
|
BIN
packages/integration-tests/user-conflict-email.png
Normal file
BIN
packages/integration-tests/user-conflict-email.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 95 KiB |
|
@ -3387,6 +3387,9 @@ importers:
|
||||||
|
|
||||||
packages/integration-tests:
|
packages/integration-tests:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@jest/test-sequencer':
|
||||||
|
specifier: ^29.5.0
|
||||||
|
version: 29.5.0
|
||||||
'@jest/types':
|
'@jest/types':
|
||||||
specifier: ^29.1.2
|
specifier: ^29.1.2
|
||||||
version: 29.1.2
|
version: 29.1.2
|
||||||
|
@ -3396,9 +3399,6 @@ importers:
|
||||||
'@logto/js':
|
'@logto/js':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@logto/language-kit':
|
|
||||||
specifier: workspace:^1.0.0
|
|
||||||
version: link:../toolkit/language-kit
|
|
||||||
'@logto/node':
|
'@logto/node':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|
Loading…
Reference in a new issue