diff --git a/__tests__/popup.ts b/__tests__/popup.ts index 86ccf0f..3f52d63 100644 --- a/__tests__/popup.ts +++ b/__tests__/popup.ts @@ -1,91 +1,137 @@ 'use strict'; -// @ts-nocheck - import * as popup from '../src/utils/popup'; +import { ENDPOINTS } from '../src/config/config'; +import { ForceMutable } from './utils'; + +const TEST_URL_INVALID = 'invalid url'; +const TEST_URL_VALID = ENDPOINTS.SPiD.DEV; + +const SCREEN: Screen = { + availHeight: 0, + availWidth: 0, + colorDepth: 0, + height: 0, + orientation: screen.orientation, + pixelDepth: 0, + width: 0, +}; describe('Popup — open', () => { + let mockWindow: ForceMutable; + beforeEach(() => { + // shallow copy of window + mockWindow = { ...window }; + }); + const defaultFeatures = 'scrollbars=yes,location=yes,status=no,menubar=no,toolbar=no,resizable=yes'; test('Fails if window undefined', () => { + // @ts-expect-error expect(() => popup.open()) .toThrow(/window was supposed to be an object but it is undefined/); }); test('Fails if window lacks "screen"', () => { - const window = {}; - expect(() => popup.open(window)) + delete (mockWindow as Partial>).screen; + + expect(() => popup.open(mockWindow, TEST_URL_VALID)) .toThrow(/window should be a valid Window object but it lacks a 'screen' property/); }); test('Fails if window lacks "open"', () => { - const window = { screen: {} }; - expect(() => popup.open(window)) + mockWindow.screen = SCREEN; + delete (mockWindow as Partial>).open; + + expect(() => popup.open(mockWindow, TEST_URL_VALID)) .toThrow(/window should be a valid Window object but it lacks an 'open' function/); }); test('Fails if url not valid', () => { - const window = { screen: {}, open: () => {} }; - expect(() => popup.open(window)).toThrow(/Invalid URL for popup/); + mockWindow.screen = SCREEN; + + expect(() => popup.open(mockWindow, TEST_URL_INVALID)).toThrow(/Invalid URL for popup/); }); test('Works with valid window and url', () => { return new Promise((resolve) => { - const open = (url, windowName, features) => { - expect(url).toBe('http://example.com'); + const open = (url: string, windowName: string, features: string) => { + expect(url).toBe(TEST_URL_VALID); expect(windowName).toBe(''); expect(features) .toBe(defaultFeatures); - resolve(); + resolve(''); }; - const window = { screen: {}, open }; - popup.open(window, 'http://example.com'); + const spy = jest.fn().mockImplementation(open); + + mockWindow.screen = SCREEN; + mockWindow.open = spy; + popup.open(mockWindow, TEST_URL_VALID); }); }); test('Works with valid window, url and windowName', () => { + const EXPECTED_WINDOW_NAME = 'FooBar'; + return new Promise((resolve) => { - const open = (url, windowName, features) => { - expect(url).toBe('http://example.com'); - expect(windowName).toBe('FooBar'); + const open = (url: string, windowName: string, features: string) => { + expect(url).toBe(TEST_URL_VALID); + expect(windowName).toBe(EXPECTED_WINDOW_NAME); expect(features) .toBe(defaultFeatures); - resolve(); + resolve(''); }; - const window = { screen: {}, open }; - popup.open(window, 'http://example.com', 'FooBar'); + const spy = jest.fn().mockImplementation(open); + + mockWindow.screen = SCREEN; + mockWindow.open = spy; + popup.open(mockWindow, TEST_URL_VALID, EXPECTED_WINDOW_NAME); }); }); test('Works with valid window, url, windowName and features', () => { + const EXPECTED_WINDOW_NAME = 'FooBar'; + const FEATURES = { foo: 'bar' }; + return new Promise((resolve) => { - const open = (url, windowName, features) => { - expect(url).toBe('http://example.com'); - expect(windowName).toBe('FooBar'); + const open = (url: string, windowName: string, features: string) => { + expect(url).toBe(TEST_URL_VALID); + expect(windowName).toBe(EXPECTED_WINDOW_NAME); expect(features) .toBe(defaultFeatures + ',foo=bar'); - resolve(); + resolve(''); }; - const window = { screen: {}, open }; - popup.open(window, 'http://example.com', 'FooBar', { foo: 'bar' }); + const spy = jest.fn().mockImplementation(open); + + mockWindow.screen = SCREEN; + mockWindow.open = spy; + popup.open(mockWindow, TEST_URL_VALID, EXPECTED_WINDOW_NAME, FEATURES); }); }); test('Works with valid window, url, windowName, features and setting left+right', () => { + const EXPECTED_WINDOW_NAME = 'FooBar'; + return new Promise((resolve) => { - const screen = { width: 400, height: 300 }; + const screenSetup = { width: 400, height: 300 }; const desiredFeatures = { width: 100, height: 200 }; - const open = (url, windowName, features) => { - expect(url).toBe('http://example.com'); - expect(windowName).toBe('FooBar'); - const left = (screen.width - desiredFeatures.width) / 2; - const top = (screen.height - desiredFeatures.height) / 2; + const open = (url: string, windowName: string, features: string) => { + expect(url).toBe(TEST_URL_VALID); + expect(windowName).toBe(EXPECTED_WINDOW_NAME); + const left = (screenSetup.width - desiredFeatures.width) / 2; + const top = (screenSetup.height - desiredFeatures.height) / 2; expect(features) .toBe(defaultFeatures + `,width=100,height=200,left=${left},top=${top}`); - resolve(); + resolve(''); + }; + const spy = jest.fn().mockImplementation(open); + + mockWindow.screen = { + ...SCREEN, + ...screenSetup, }; - const window = { screen, open }; - popup.open(window, 'http://example.com', 'FooBar', desiredFeatures); + mockWindow.open = spy; + popup.open(mockWindow, TEST_URL_VALID, EXPECTED_WINDOW_NAME, desiredFeatures); }); }); }); diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 2fecefc..a999adf 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -2,9 +2,14 @@ * See LICENSE.md in the project root. */ -// @ts-nocheck +export type ForceMutable = { + -readonly [K in keyof T]: T[K]; +}; + +export const throwingFnMsg = 'TEST THROW'; +export const throwingFn = () => { throw new Error(throwingFnMsg);}; -function stringify(search) { +function stringifySearchParams(search: URLSearchParams): string { const keys = [...new Set(search.keys())]; keys.sort(); return keys.map(k => { @@ -20,7 +25,7 @@ function stringify(search) { * @param {string} second - the second url * @return {Array} - returns the URL objects that are made from first and second */ -export function compareUrls(first, second) { +export function compareUrls(first: string, second: string): URL[] { const firstUrl = new URL(first); const secondUrl = new URL(second); expect(firstUrl).toBeDefined(); @@ -35,7 +40,7 @@ export function compareUrls(first, second) { expect(firstUrl.pathname).toBe(secondUrl.pathname); expect(firstUrl.port).toBe(secondUrl.port); expect(firstUrl.protocol).toBe(secondUrl.protocol); - expect(stringify(firstUrl.searchParams)).toEqual(stringify(secondUrl.searchParams)); + expect(stringifySearchParams(firstUrl.searchParams)).toEqual(stringifySearchParams(secondUrl.searchParams)); return [firstUrl, secondUrl]; } @@ -62,11 +67,11 @@ const sessionResponse = { }; const sessionServiceAccess = { entitled: true, - allowedFeatures: ["existing"], + allowedFeatures: ['existing'], ttl: 10, userId: 12345, uuid: 'aaaaaaaa-de42-5c4b-80ee-eeeeeeeeeeee', - sig: 'ZUtX5e7WJcLl69m-puKJlFc413ZPi7wnMLTa_M9TFiU.eyJlbnRpdGxlZCI6dHJ1ZSwiYWxsb3dlZEZlYXR1cmVzIjpbImZlYXR1cmUtMSIsInByb2R1Y3RpZC0xIl0sInR0bCI6MTAsInVzZXJJZCI6MTIzNDUsInV1aWQiOiJ1c2VyVXVpZCIsImFsZ29yaXRobSI6IkhNQUMtU0hBMjU2In0' + sig: 'ZUtX5e7WJcLl69m-puKJlFc413ZPi7wnMLTa_M9TFiU.eyJlbnRpdGxlZCI6dHJ1ZSwiYWxsb3dlZEZlYXR1cmVzIjpbImZlYXR1cmUtMSIsInByb2R1Y3RpZC0xIl0sInR0bCI6MTAsInVzZXJJZCI6MTIzNDUsInV1aWQiOiJ1c2VyVXVpZCIsImFsZ29yaXRobSI6IkhNQUMtU0hBMjU2In0', }; const sessionServiceNoAccess = { @@ -75,7 +80,7 @@ const sessionServiceNoAccess = { ttl: 0, userId: 12345, uuid: 'aaaaaaaa-de42-5c4b-80ee-eeeeeeeeeeee', - sig: 'Rqf5fQ-gXNOdrsegajNgTOzju5z9-0v92v-PGCnL5P8.eyJlbnRpdGxlZCI6ZmFsc2UsImFsbG93ZWRGZWF0dXJlcyI6W10sInR0bCI6MCwidXNlcklkIjoxMjM0NSwidXVpZCI6InVzZXJVdWlkIiwiYWxnb3JpdGhtIjoiSE1BQy1TSEEyNTYifQ' + sig: 'Rqf5fQ-gXNOdrsegajNgTOzju5z9-0v92v-PGCnL5P8.eyJlbnRpdGxlZCI6ZmFsc2UsImFsbG93ZWRGZWF0dXJlcyI6W10sInR0bCI6MCwidXNlcklkIjoxMjM0NSwidXVpZCI6InVzZXJVdWlkIiwiYWxnb3JpdGhtIjoiSE1BQy1TSEEyNTYifQ', }; export const Fixtures = { diff --git a/src/clients/RESTClient.ts b/src/clients/RESTClient.ts index 9b1ae16..436a1f7 100644 --- a/src/clients/RESTClient.ts +++ b/src/clients/RESTClient.ts @@ -160,12 +160,13 @@ export class RESTClient { logFn(this.log!, 'Request:', fetchOptions.method?.toUpperCase(), fullUrl); logFn(this.log!, 'Request Headers:', fetchOptions.headers); logFn(this.log!, 'Request Body:', fetchOptions.body); + try { const response = await this.fetch!(fullUrl, fetchOptions); logFn(this.log!, 'Response Code:', response.status, response.statusText); if (!response.ok) { // status code not in range 200-299 - throw new SDKError(`API call failed with: ${response.code} ${response.statusText}`); + throw new SDKError(`API call failed with: ${response.code} ${response.statusText}`, { code: response.code}); } const responseObject = await response.json(); logFn(this.log!, 'Response Parsed:', responseObject); @@ -213,8 +214,8 @@ export class RESTClient { * @param {object} defaultParams - Default params * @returns {string} Query string */ - static search(query: GenericObject, useDefaultParams: boolean, defaultParams: GenericObject) { - const params = useDefaultParams ? cloneDefined(defaultParams, query) : cloneDefined(query); + static search(query: GenericObject, useDefaultParams?: boolean, defaultParams?: GenericObject) { + const params = useDefaultParams ? cloneDefined(defaultParams || {}, query) : cloneDefined(query); return Object.keys(params) .filter(p => params[p] !== '') .map(p => `${encode(p)}=${encode(params[p] as string)}`) diff --git a/src/utils/popup.ts b/src/utils/popup.ts index c6e83d3..99861e4 100644 --- a/src/utils/popup.ts +++ b/src/utils/popup.ts @@ -4,17 +4,19 @@ import { assert, isObject, isUrl, isFunction } from './validate'; import { GenericObject } from './types'; +type FeatureActivation = 'yes' | 'no'; + interface WindowFeatures { left?: number; top?: number; width?: number; height?: number; - menubar?: boolean; - toolbar?: boolean; - location?: boolean; - status?: boolean; - resizable?: boolean; - scrollbars?: boolean; + menubar?: FeatureActivation; + toolbar?: FeatureActivation; + location?: FeatureActivation; + status?: FeatureActivation; + resizable?: FeatureActivation; + scrollbars?: FeatureActivation; [key: string]: unknown } @@ -33,12 +35,12 @@ function serialize(obj: GenericObject) { } const defaultWindowFeatures: WindowFeatures = { - scrollbars: true, - location: true, - status: false, - menubar: false, - toolbar: false, - resizable: true, + scrollbars: 'yes', + location: 'yes', + status: 'no', + menubar: 'no', + toolbar: 'no', + resizable: 'yes', }; /** @@ -66,6 +68,6 @@ export function open(parentWindow: Window, url: string, windowName = '', windowF if (Number.isFinite(windowFeatures.height) && Number.isFinite(height)) { windowFeatures.top = (height - (windowFeatures.height || 0)) / 2; } - const features = serialize(windowFeatures); + const features = serialize({ ...defaultWindowFeatures, ...windowFeatures }); return parentWindow.open(url, windowName, features); }