0
Fork 0
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:
Gao Sun 2023-09-15 00:50:13 +08:00
parent e1fac554db
commit 092b3d7aea
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
4 changed files with 72 additions and 72 deletions

View file

@ -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",

View file

@ -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();
}); });
}); });

View file

@ -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();
}); });
}); });

View file

@ -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.');
} }
} }