Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add-tests #15

Merged
merged 6 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-fans-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': patch
---

fixes the checks to determine what node state we are in based on the response from p1
4 changes: 2 additions & 2 deletions .husky/install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') {
process.exit(0);
}
const husky = (await import('husky')).default;
husky();
const husky = (await import('husky')).default
console.log(husky())
21 changes: 12 additions & 9 deletions e2e/davinci-app/components/password.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PasswordCollector, Updater } from '@forgerock/davinci-client/types';
import { dotToCamelCase } from '../helper.js';

export default function passwordComponent(
formEl: HTMLFormElement,
Expand All @@ -8,19 +9,21 @@ export default function passwordComponent(
const label = document.createElement('label');
const input = document.createElement('input');

label.htmlFor = collector.output.key;
label.htmlFor = dotToCamelCase(collector.output.key);
label.innerText = collector.output.label;
input.type = 'password';
input.id = collector.output.key;
input.name = collector.output.key;
input.id = dotToCamelCase(collector.output.key);
input.name = dotToCamelCase(collector.output.key);

formEl?.appendChild(label);
formEl?.appendChild(input);

formEl?.querySelector(`#${collector.output.key}`)?.addEventListener('blur', (event: Event) => {
const error = updater((event.target as HTMLInputElement).value);
if (error && 'error' in error) {
console.error(error.error.message);
}
});
formEl
?.querySelector(`#${dotToCamelCase(collector.output.key)}`)
?.addEventListener('blur', (event: Event) => {
const error = updater((event.target as HTMLInputElement).value);
if (error && 'error' in error) {
console.error(error.error.message);
}
});
}
3 changes: 2 additions & 1 deletion e2e/davinci-app/components/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import type {
ValidatedTextCollector,
Updater,
} from '@forgerock/davinci-client/types';
import { dotToCamelCase } from '../helper.js';

export default function usernameComponent(
formEl: HTMLFormElement,
collector: TextCollector | ValidatedTextCollector,
updater: Updater,
) {
const collectorKey = collector.output.key;
const collectorKey = dotToCamelCase(collector.output.key);
const label = document.createElement('label');
const input = document.createElement('input');

Expand Down
8 changes: 8 additions & 0 deletions e2e/davinci-app/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function dotToCamelCase(str: string) {
return str
.split('.')
.map((part: string, index: number) =>
index === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),
)
.join('');
}
6 changes: 3 additions & 3 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ const urlParams = new URLSearchParams(window.location.search);

const loginBtn = document.getElementById('logoutButton') as HTMLButtonElement;
loginBtn.addEventListener('click', async () => {
await FRUser.logout({ logoutRedirectUri: window.location.href });
await FRUser.logout({ logoutRedirectUri: `${window.location.origin}/` });
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because of the above changes , fixed this to use origin so it doesnt include the clientId param


window.location.reload();
//window.location.reload();
});
}

Expand Down Expand Up @@ -204,7 +204,7 @@ const urlParams = new URLSearchParams(window.location.search);
node = await davinciClient.start({ query });
} else {
node = resumed;
console.log('node is reusmed');
console.log('node is resumed');
console.log(node);
}

Expand Down
7 changes: 6 additions & 1 deletion e2e/davinci-app/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
"moduleResolution": "Bundler"
},
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
"include": ["./main.ts", "./server-configs.ts", "components/**/*.ts"],
"include": [
"./main.ts",
"./helper.ts",
"./server-configs.ts",
"components/**/*.ts"
],
"references": [
{
"path": "../../packages/davinci-client/tsconfig.lib.json"
Expand Down
17 changes: 17 additions & 0 deletions e2e/davinci-suites/src/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ test('Test happy paths on test page', async ({ page }) => {

const accessToken = await page.locator('#accessTokenValue').innerText();
await expect(accessToken).toBeTruthy();

const logoutButton = page.getByRole('button', { name: 'Logout' });
await expect(logoutButton).toBeVisible();
const revokeCall = page.waitForResponse((response) => {
if (response.url().includes('/revoke') && response.status() === 200) {
return true;
}
});
const signoff = page.waitForResponse((response) => {
if (response.url().includes('/signoff') && response.status() === 302) {
return true;
}
});
await logoutButton.click();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extended this test to include logout for my own sanity.

await revokeCall;
await signoff;
await expect(page.getByText('Username/Password Form')).toBeVisible();
});
test('ensure query params passed to start are sent off in authorize call', async ({ page }) => {
const { navigate } = asyncEvents(page);
Expand Down
67 changes: 67 additions & 0 deletions e2e/davinci-suites/src/form-fields.test_off.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from '@playwright/test';

test('Should render form fields', async ({ page }) => {
await page.goto('http://localhost:5829/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
await page.getByRole('heading', { name: 'Select Test Form' }).click();
await page.getByRole('button', { name: 'Form Fields' }).click();
await page.getByRole('textbox', { name: 'Text Input Label' }).click();
await page.getByRole('textbox', { name: 'Text Input Label' }).click();

const txtInput = page.getByRole('textbox', { name: 'Text Input Label' });
await txtInput.fill('This is some text');
expect(txtInput).toHaveValue('This is some text');

const flowLink = page.getByRole('button', { name: 'Flow Link' });
await flowLink.click();

const flowButton = page.getByRole('button', { name: 'Flow Button' });
await flowButton.click();

const requestPromise = page.waitForRequest((request) => request.url().includes('/customForm'));
await page.getByRole('button', { name: 'Submit' }).click();
const request = await requestPromise;
const parsedData = JSON.parse(request.postData());
const data = parsedData.parameters.data;
expect(data.actionKey).toBe('submit');
expect(data.formData).toEqual({
['text-input-key']: 'This is some text',
['dropdown-field-key']: '',
['radio-group-key']: '',
});
});

test('should render form validation fields', async ({ page }) => {
await page.goto('http://localhost:5829/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
await expect(page.getByRole('link', { name: 'Vite logo' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Form Validation' })).toBeVisible();
await expect(page.locator('#form')).toContainText('Form Validation');
await page.getByRole('button', { name: 'Form Validation' }).click();
await expect(page.getByRole('heading', { name: 'Form Fields Validation' })).toBeVisible();

await page.getByRole('textbox', { name: 'Username' }).fill('sdk-user');
await expect(page.getByRole('textbox', { name: 'Username' })).toHaveValue('sdk-user');

const password = page.getByRole('textbox', { name: 'Password' });
await password.type('password');
await expect(password).toHaveValue('password');

await page.getByRole('textbox', { name: 'Email Address' }).fill('[email protected]');
await expect(page.getByRole('textbox', { name: 'Email Address' })).toHaveValue(
'[email protected]',
);

const requestPromise = page.waitForRequest((request) => request.url().includes('/customForm'));
await page.getByRole('button', { name: 'Submit' }).click();

const request = await requestPromise;
const parsedData = JSON.parse(request.postData());

const data = parsedData.parameters.data;

expect(data.actionKey).toBe('submit');
expect(data.formData).toEqual({
'user.username': 'sdk-user',
'user.password': 'password',
'user.email': '[email protected]',
});
});
80 changes: 44 additions & 36 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
// Pass store methods to the client
subscribe: store.subscribe,

/**
* Social Login Handler
* Use this as part of an event when clicking on
* a social login button. Pass in the collector responsible
* for the social login being started.
*
* This method will save the `continueUrl`
* and then replace the window with the authenticate
* url from the collector
*
* Can return an error when no continue url is found
* or no authenticate url is found in the collectors
*
* @method: externalIdp
* @param collector IdpCollector
* @returns {function}
*/
externalIdp: (collector: IdpCollector) => {
const rootState: RootState = store.getState();

const serverSlice = nodeSlice.selectors.selectServer(rootState);

return () => authorize(serverSlice, collector);
},

/**
* @method flow - Method for initiating a new flow, different than current flow
* @param {DaVinciAction} action - the action to initiate the flow
Expand Down Expand Up @@ -97,6 +122,15 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
return node;
},

/**
* @method: resume - Resume a social login flow when returned to application
* @returns unknown
*/
resume: async ({ continueToken }: { continueToken: string }) => {
const node = store.dispatch(davinciApi.endpoints.resume.initiate({ continueToken }));
return node;
},

/**
* @method start - Method for initiating a DaVinci flow
* @returns {Promise} - a promise that initiates a DaVinci flow and returns a node
Expand Down Expand Up @@ -140,13 +174,20 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
};
}

if (collectorToUpdate.category !== 'SingleValueCollector') {
console.error('Collector is not a SingleValueCollector and cannot be updated');
if (
collectorToUpdate.category !== 'MultiValueCollector' &&
collectorToUpdate.category !== 'SingleValueCollector' &&
collectorToUpdate.category !== 'ValidatedSingleValueCollector'
) {
console.error(
'Collector is not a MultiValueCollector, SingleValueCollector or ValidatedSingleValueCollector and cannot be updated',
);
return function () {
return {
type: 'internal_error',
error: {
message: 'Collector is not a SingleValueCollector and cannot be updated',
message:
'Collector is not a SingleValueCollector or ValidatedSingleValueCollector and cannot be updated',
type: 'state_error',
},
} as const;
Expand Down Expand Up @@ -271,39 +312,6 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
return nodeSlice.selectors.selectServer(state);
},

/**
* @method: resume - Resume a social login flow when returned to application
* @returns unknown
*/
resume: async ({ continueToken }: { continueToken: string }) => {
const node = store.dispatch(davinciApi.endpoints.resume.initiate({ continueToken }));
return node;
},
/**
* Social Login Handler
* Use this as part of an event when clicking on
* a social login button. Pass in the collector responsible
* for the social login being started.
*
* This method will save the `continueUrl`
* and then replace the window with the authenticate
* url from the collector
*
* Can return an error when no continue url is found
* or no authenticate url is found in the collectors
*
* @method: externalIdp
* @param collector IdpCollector
* @returns {function}
*/
externalIdp: (collector: IdpCollector) => {
const rootState: RootState = store.getState();

const serverSlice = nodeSlice.selectors.selectServer(rootState);

return () => authorize(serverSlice, collector);
},

/**
* Utilities to help query cached responses from server
*/
Expand Down
28 changes: 22 additions & 6 deletions packages/davinci-client/src/lib/davinci.utils.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is why i'm adding a changeset.

Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import { InternalErrorResponse } from './client.types.js';
export function transformSubmitRequest(node: ContinueNode): DaVinciRequest {
// Filter out ActionCollectors as they are not used in form submissions
const collectors = node.client?.collectors?.filter(
(collector) => collector.category === 'SingleValueCollector',
(collector) =>
collector.category === 'MultiValueCollector' ||
collector.category === 'SingleValueCollector' ||
collector.category === 'ValidatedSingleValueCollector',
);

const formData = collectors?.reduce<{
[key: string]: string | number | boolean;
[key: string]: string | number | boolean | (string | number | boolean)[];
}>((acc, collector) => {
acc[collector.input.key] = collector.input.value;
return acc;
Expand Down Expand Up @@ -175,12 +178,25 @@ export function handleResponse(cacheEntry: DaVinciCacheEntry, dispatch: Dispatch
*/
if (cacheEntry.isSuccess) {
const requestId = cacheEntry.requestId;
if ('eventName' in cacheEntry.data && cacheEntry.data.eventName === 'continue') {
const data = cacheEntry.data as DaVinciNextResponse;
dispatch(nodeSlice.actions.next({ data, requestId, httpStatus: status }));
} else if ('session' in cacheEntry.data || 'authorizeResponse' in cacheEntry.data) {
const hasNextUrl = () => {
const data = cacheEntry.data;

if ('_links' in data) {
if ('next' in data._links) {
if ('href' in data._links.next) {
return true;
}
}
}
return false;
};
Comment on lines +181 to +192
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be a pure, stateless utility that could be used for fetching any property within _links. This could be reused in other portions of the project.


if ('session' in cacheEntry.data || 'authorizeResponse' in cacheEntry.data) {
const data = cacheEntry.data as DaVinciSuccessResponse;
dispatch(nodeSlice.actions.success({ data, requestId, httpStatus: status }));
} else if (hasNextUrl()) {
const data = cacheEntry.data as DaVinciNextResponse;
dispatch(nodeSlice.actions.next({ data, requestId, httpStatus: status }));
} else {
// If we got here, the response type is unknown and therefore an unrecoverable failure
const data = cacheEntry.data as DaVinciFailureResponse;
Expand Down
6 changes: 5 additions & 1 deletion packages/davinci-client/src/lib/node.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build

// *Some* collectors may have default or existing data to display
const data = action.payload.formData[field.key];

// Match specific collectors
switch (field.type) {
case 'CHECKBOX':
Expand Down Expand Up @@ -165,7 +166,10 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build
throw new Error('Value argument cannot be undefined');
}

if (collector.category === 'SingleValueCollector') {
if (
collector.category === 'SingleValueCollector' ||
collector.category === 'ValidatedSingleValueCollector'
) {
if (Array.isArray(action.payload.value)) {
throw new Error('SingleValueCollector does not accept an array');
}
Expand Down
Loading