mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor: ui -> experience (part 1)
This commit is contained in:
parent
e1fac554db
commit
092b3d7aea
4 changed files with 72 additions and 72 deletions
|
@ -14,7 +14,7 @@
|
||||||
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
|
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
|
||||||
"test": "pnpm build && pnpm test:api && pnpm test:experience && pnpm test:console",
|
"test": "pnpm build && pnpm test:api && pnpm test:experience && pnpm test:console",
|
||||||
"test:api": "pnpm test:only -i ./lib/tests/api/",
|
"test:api": "pnpm test:only -i ./lib/tests/api/",
|
||||||
"test:experience": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/flows/",
|
"test:experience": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/experience/",
|
||||||
"test:console": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/console/",
|
"test:console": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/console/",
|
||||||
"lint": "eslint --ext .ts src",
|
"lint": "eslint --ext .ts src",
|
||||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { SignInMode, SignInIdentifier, ConnectorType } from '@logto/schemas';
|
||||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||||
import { demoAppUrl } from '#src/constants.js';
|
import { demoAppUrl } from '#src/constants.js';
|
||||||
import { clearConnectorsByTypes } from '#src/helpers/connector.js';
|
import { clearConnectorsByTypes } from '#src/helpers/connector.js';
|
||||||
import ExpectFlows from '#src/ui-helpers/expect-flows.js';
|
import ExpectExperience from '#src/ui-helpers/expect-experience.js';
|
||||||
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
username: 'test_bootstrap',
|
username: 'test_bootstrap',
|
||||||
|
@ -42,19 +42,19 @@ describe('smoke testing on the demo app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to create a new account with a credential preset', async () => {
|
it('should be able to create a new account with a credential preset', async () => {
|
||||||
const journey = new ExpectFlows(await browser.newPage());
|
const experience = new ExpectExperience(await browser.newPage());
|
||||||
|
|
||||||
// Open the demo app and navigate to the register page
|
// Open the demo app and navigate to the register page
|
||||||
await journey.startWith(demoAppUrl, 'register');
|
await experience.startWith(demoAppUrl, 'register');
|
||||||
await journey.toFillInput('identifier', credentials.username, { submit: true });
|
await experience.toFillInput('identifier', credentials.username, { submit: true });
|
||||||
|
|
||||||
// Simple password tests
|
// Simple password tests
|
||||||
journey.toBeAt('register/password');
|
experience.toBeAt('register/password');
|
||||||
await journey.toFillPasswords(
|
await experience.toFillPasswords(
|
||||||
[credentials.pwnedPassword, 'simple password'],
|
[credentials.pwnedPassword, 'simple password'],
|
||||||
credentials.password
|
credentials.password
|
||||||
);
|
);
|
||||||
|
|
||||||
await journey.verifyThenEnd();
|
await experience.verifyThenEnd();
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -5,7 +5,7 @@ import { ConnectorType, SignInIdentifier, SignInMode } from '@logto/schemas';
|
||||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||||
import { demoAppUrl } from '#src/constants.js';
|
import { demoAppUrl } from '#src/constants.js';
|
||||||
import { clearConnectorsByTypes, setEmailConnector } from '#src/helpers/connector.js';
|
import { clearConnectorsByTypes, setEmailConnector } from '#src/helpers/connector.js';
|
||||||
import ExpectFlows from '#src/ui-helpers/expect-flows.js';
|
import ExpectExperience from '#src/ui-helpers/expect-experience.js';
|
||||||
import { waitFor } from '#src/utils.js';
|
import { waitFor } from '#src/utils.js';
|
||||||
|
|
||||||
describe('password policy', () => {
|
describe('password policy', () => {
|
||||||
|
@ -60,21 +60,21 @@ describe('password policy', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for username + password', async () => {
|
it('should work for username + password', async () => {
|
||||||
const journey = new ExpectFlows(await browser.newPage(), { forgotPassword: true });
|
const experience = new ExpectExperience(await browser.newPage(), { forgotPassword: true });
|
||||||
|
|
||||||
// Open the demo app and navigate to the register page
|
// Open the demo app and navigate to the register page
|
||||||
await journey.startWith(demoAppUrl, 'register');
|
await experience.startWith(demoAppUrl, 'register');
|
||||||
await journey.toFillInput('identifier', username, { submit: true });
|
await experience.toFillInput('identifier', username, { submit: true });
|
||||||
|
|
||||||
// Password tests
|
// Password tests
|
||||||
journey.toBeAt('register/password');
|
experience.toBeAt('register/password');
|
||||||
await journey.toFillPasswords(
|
await experience.toFillPasswords(
|
||||||
...invalidPasswords,
|
...invalidPasswords,
|
||||||
[username + 'A', /product context .* personal information/],
|
[username + 'A', /product context .* personal information/],
|
||||||
username + 'ABCD_ok'
|
username + 'ABCD_ok'
|
||||||
);
|
);
|
||||||
|
|
||||||
await journey.verifyThenEnd();
|
await experience.verifyThenEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for email + password', async () => {
|
it('should work for email + password', async () => {
|
||||||
|
@ -86,56 +86,56 @@ describe('password policy', () => {
|
||||||
verify: true,
|
verify: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const journey = new ExpectFlows(await browser.newPage(), { forgotPassword: true });
|
const experience = new ExpectExperience(await browser.newPage(), { forgotPassword: true });
|
||||||
|
|
||||||
// Open the demo app and navigate to the register page
|
// Open the demo app and navigate to the register page
|
||||||
await journey.startWith(demoAppUrl, 'register');
|
await experience.startWith(demoAppUrl, 'register');
|
||||||
|
|
||||||
// Complete verification code flow
|
// Complete verification code flow
|
||||||
await journey.toFillInput('identifier', email, { submit: true });
|
await experience.toFillInput('identifier', email, { submit: true });
|
||||||
await journey.toCompleteVerification('register');
|
await experience.toCompleteVerification('register');
|
||||||
|
|
||||||
// Wait for the password page to load
|
// Wait for the password page to load
|
||||||
await waitFor(100);
|
await waitFor(100);
|
||||||
journey.toBeAt('continue/password');
|
experience.toBeAt('continue/password');
|
||||||
await journey.toFillPasswords(
|
await experience.toFillPasswords(
|
||||||
...invalidPasswords,
|
...invalidPasswords,
|
||||||
[emailName, 'personal information'],
|
[emailName, 'personal information'],
|
||||||
emailName + 'ABCD@# $'
|
emailName + 'ABCD@# $'
|
||||||
);
|
);
|
||||||
|
|
||||||
await journey.verifyThenEnd();
|
await experience.verifyThenEnd();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for forgot password', async () => {
|
it('should work for forgot password', async () => {
|
||||||
const journey = new ExpectFlows(await browser.newPage(), { forgotPassword: true });
|
const experience = new ExpectExperience(await browser.newPage(), { forgotPassword: true });
|
||||||
|
|
||||||
// Open the demo app and navigate to the register page
|
// Open the demo app and navigate to the register page
|
||||||
await journey.startWith(demoAppUrl, 'sign-in');
|
await experience.startWith(demoAppUrl, 'sign-in');
|
||||||
|
|
||||||
// Click the forgot password link
|
// Click the forgot password link
|
||||||
await journey.toFillInput('identifier', email, { submit: true });
|
await experience.toFillInput('identifier', email, { submit: true });
|
||||||
await journey.toClick('a', 'Forgot your password');
|
await experience.toClick('a', 'Forgot your password');
|
||||||
|
|
||||||
// Submit to continue
|
// Submit to continue
|
||||||
await journey.toClickSubmit();
|
await experience.toClickSubmit();
|
||||||
|
|
||||||
// Complete verification code flow
|
// Complete verification code flow
|
||||||
await journey.toCompleteVerification('forgot-password');
|
await experience.toCompleteVerification('forgot-password');
|
||||||
|
|
||||||
// Wait for the password page to load
|
// Wait for the password page to load
|
||||||
await waitFor(100);
|
await waitFor(100);
|
||||||
journey.toBeAt('forgot-password/reset');
|
experience.toBeAt('forgot-password/reset');
|
||||||
await journey.toFillPasswords(
|
await experience.toFillPasswords(
|
||||||
...invalidPasswords,
|
...invalidPasswords,
|
||||||
[emailName, 'personal information'],
|
[emailName, 'personal information'],
|
||||||
[emailName + 'ABCD@# $', 'be the same as'],
|
[emailName + 'ABCD@# $', 'be the same as'],
|
||||||
emailName + 'ABCD135'
|
emailName + 'ABCD135'
|
||||||
);
|
);
|
||||||
|
|
||||||
journey.toBeAt('sign-in');
|
experience.toBeAt('sign-in');
|
||||||
await journey.toFillInput('identifier', email, { submit: true });
|
await experience.toFillInput('identifier', email, { submit: true });
|
||||||
await journey.toFillInput('password', emailName + 'ABCD135', { submit: true });
|
await experience.toFillInput('password', emailName + 'ABCD135', { submit: true });
|
||||||
await journey.verifyThenEnd();
|
await experience.verifyThenEnd();
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -10,17 +10,17 @@ const demoAppUrl = appendPath(new URL(logtoUrl), 'demo-app');
|
||||||
/** Remove the query string together with the `?` from a URL string. */
|
/** Remove the query string together with the `?` from a URL string. */
|
||||||
const stripQuery = (url: string) => url.split('?')[0];
|
const stripQuery = (url: string) => url.split('?')[0];
|
||||||
|
|
||||||
export type FlowsType = 'sign-in' | 'register' | 'continue' | 'forgot-password';
|
export type ExperienceType = 'sign-in' | 'register' | 'continue' | 'forgot-password';
|
||||||
|
|
||||||
export type FlowsPath =
|
export type ExperiencePath =
|
||||||
| FlowsType
|
| ExperienceType
|
||||||
| `${FlowsType}/password`
|
| `${ExperienceType}/password`
|
||||||
| `${FlowsType}/verify`
|
| `${ExperienceType}/verify`
|
||||||
| `${FlowsType}/verification-code`
|
| `${ExperienceType}/verification-code`
|
||||||
| `forgot-password/reset`;
|
| `forgot-password/reset`;
|
||||||
|
|
||||||
export type ExpectFlowsOptions = {
|
export type ExpectExperienceOptions = {
|
||||||
/** The URL of the flows endpoint. */
|
/** The URL of the experience endpoint. */
|
||||||
endpoint?: URL;
|
endpoint?: URL;
|
||||||
/**
|
/**
|
||||||
* Whether the forgot password flow is enabled.
|
* Whether the forgot password flow is enabled.
|
||||||
|
@ -30,30 +30,30 @@ export type ExpectFlowsOptions = {
|
||||||
forgotPassword?: boolean;
|
forgotPassword?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OngoingFlows = {
|
type OngoingExperience = {
|
||||||
type: FlowsType;
|
type: ExperienceType;
|
||||||
initialUrl: URL;
|
initialUrl: URL;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that provides:
|
* A class that provides:
|
||||||
*
|
*
|
||||||
* - A set of methods to navigate to a specific page for a flows.
|
* - A set of methods to navigate to a specific page for a experience.
|
||||||
* - A set of methods to assert the state of a flows and its side effects.
|
* - A set of methods to assert the state of a experience and its side effects.
|
||||||
*/
|
*/
|
||||||
export default class ExpectFlows extends ExpectPage {
|
export default class ExpectExperience extends ExpectPage {
|
||||||
readonly options: Required<ExpectFlowsOptions>;
|
readonly options: Required<ExpectExperienceOptions>;
|
||||||
|
|
||||||
protected get flowsType() {
|
protected get experienceType() {
|
||||||
if (this.#ongoing === undefined) {
|
if (this.#ongoing === undefined) {
|
||||||
return this.throwNoOngoingFlowsError();
|
return this.throwNoOngoingExperienceError();
|
||||||
}
|
}
|
||||||
return this.#ongoing.type;
|
return this.#ongoing.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ongoing?: OngoingFlows;
|
#ongoing?: OngoingExperience;
|
||||||
|
|
||||||
constructor(thePage = global.page, options: ExpectFlowsOptions = {}) {
|
constructor(thePage = global.page, options: ExpectExperienceOptions = {}) {
|
||||||
super(thePage);
|
super(thePage);
|
||||||
this.options = {
|
this.options = {
|
||||||
endpoint: new URL(logtoUrl),
|
endpoint: new URL(logtoUrl),
|
||||||
|
@ -63,16 +63,16 @@ export default class ExpectFlows extends ExpectPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start flows with the given initial URL. Expect the initial URL is protected by Logto, and
|
* Start experience with the given initial URL. Expect the initial URL is protected by Logto, and
|
||||||
* navigate to the flows sign-in page if unauthenticated.
|
* navigate to the experience sign-in page if unauthenticated.
|
||||||
*
|
*
|
||||||
* If the flows can be started, the instance will be marked as ongoing.
|
* If the experience can be started, the instance will be marked as ongoing.
|
||||||
*
|
*
|
||||||
* @param initialUrl The initial URL to start the flows with.
|
* @param initialUrl The initial URL to start the experience with.
|
||||||
* @param type The type of flows to expect. If it's `register`, it will try to click the "Create
|
* @param type The type of experience to expect. If it's `register`, it will try to click the "Create
|
||||||
* account" link on the sign-in page.
|
* account" link on the sign-in page.
|
||||||
*/
|
*/
|
||||||
async startWith(initialUrl = demoAppUrl, type: FlowsType = 'sign-in') {
|
async startWith(initialUrl = demoAppUrl, type: ExperienceType = 'sign-in') {
|
||||||
await this.toStart(initialUrl);
|
await this.toStart(initialUrl);
|
||||||
this.toBeAt('sign-in');
|
this.toBeAt('sign-in');
|
||||||
|
|
||||||
|
@ -85,14 +85,14 @@ export default class ExpectFlows extends ExpectPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the flows is ongoing and the page is at the initial URL; then try to click the "sign out"
|
* Ensure the experience is ongoing and the page is at the initial URL; then try to click the "sign out"
|
||||||
* button (case-insensitive) and close the page.
|
* button (case-insensitive) and close the page.
|
||||||
*
|
*
|
||||||
* It will clear the ongoing flows if the flows is ended successfully.
|
* It will clear the ongoing experience if the experience is ended successfully.
|
||||||
*/
|
*/
|
||||||
async verifyThenEnd() {
|
async verifyThenEnd() {
|
||||||
if (this.#ongoing === undefined) {
|
if (this.#ongoing === undefined) {
|
||||||
return this.throwNoOngoingFlowsError();
|
return this.throwNoOngoingExperienceError();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toMatchUrl(this.#ongoing.initialUrl);
|
this.toMatchUrl(this.#ongoing.initialUrl);
|
||||||
|
@ -103,22 +103,22 @@ export default class ExpectFlows extends ExpectPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert the page is at the given flows path.
|
* Assert the page is at the given experience path.
|
||||||
*
|
*
|
||||||
* @param pathname The flows path to assert.
|
* @param pathname The experience path to assert.
|
||||||
*/
|
*/
|
||||||
toBeAt(pathname: FlowsPath) {
|
toBeAt(pathname: ExperiencePath) {
|
||||||
const stripped = stripQuery(this.page.url());
|
const stripped = stripQuery(this.page.url());
|
||||||
expect(stripped).toBe(this.buildFlowsUrl(pathname).href);
|
expect(stripped).toBe(this.buildExperienceUrl(pathname).href);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert the page is at the verification code page and fill the verification code input with the
|
* Assert the page is at the verification code page and fill the verification code input with the
|
||||||
* code from Logto database.
|
* code from Logto database.
|
||||||
*
|
*
|
||||||
* @param type The type of flows to expect.
|
* @param type The type of experience to expect.
|
||||||
*/
|
*/
|
||||||
async toCompleteVerification(type: FlowsType) {
|
async toCompleteVerification(type: ExperienceType) {
|
||||||
this.toBeAt(`${type}/verification-code`);
|
this.toBeAt(`${type}/verification-code`);
|
||||||
const { code } = await readVerificationCode();
|
const { code } = await readVerificationCode();
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ export default class ExpectFlows extends ExpectPage {
|
||||||
* "simple password" (case-insensitive), and the second password is expected to be accepted.
|
* "simple password" (case-insensitive), and the second password is expected to be accepted.
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* await journey.toFillPasswords(
|
* await experience.toFillPasswords(
|
||||||
* [credentials.pwnedPassword, 'simple password'],
|
* [credentials.pwnedPassword, 'simple password'],
|
||||||
* credentials.password,
|
* credentials.password,
|
||||||
* );
|
* );
|
||||||
|
@ -175,12 +175,12 @@ export default class ExpectFlows extends ExpectPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a full flows URL from a pathname. */
|
/** Build a full experience URL from a pathname. */
|
||||||
protected buildFlowsUrl(pathname = '') {
|
protected buildExperienceUrl(pathname = '') {
|
||||||
return appendPath(this.options.endpoint, pathname);
|
return appendPath(this.options.endpoint, pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected throwNoOngoingFlowsError() {
|
protected throwNoOngoingExperienceError() {
|
||||||
return this.throwError('The flows has not started yet. Use `startWith` to start the flows.');
|
return this.throwError('The experience has not started yet. Use `startWith` to start the experience.');
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue