From 3d96be0d261b265128f075c7beb59ff97f4b5df6 Mon Sep 17 00:00:00 2001 From: Kenny Gray Date: Sat, 5 Feb 2022 19:47:56 +0000 Subject: [PATCH] feat: added new scenarios endpoint Added new scenarios endpoint so that it's easier for other UIs to be built on top of data-mocks-server --- README.md | 3 +- example/index.ts | 2 +- src/Html.tsx | 4 +- src/apis.ts | 17 ++++- src/index.spec.ts | 149 ++++++++++++++++++++++++++++++++++++- src/run.ts | 15 +++- src/types.ts | 11 ++- src/ui.tsx | 74 +++++------------- src/utils/get-scenarios.ts | 58 +++++++++++++++ 9 files changed, 269 insertions(+), 64 deletions(-) create mode 100644 src/utils/get-scenarios.ts diff --git a/README.md b/README.md index 4d2d709..248b58a 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Returns an http server, with an additional kill method #### options -> `{ port, uiPath, modifyScenariosPath, resetScenariosPath }` | defaults to `{}` +> `{ port, uiPath, modifyScenariosPath, resetScenariosPath, scenariosPath }` | defaults to `{}` @@ -110,6 +110,7 @@ Returns an http server, with an additional kill method | uiPath | `string` | `/` | Path that the UI will load on. `http://localhost:{port}{uiPath}` | | modifyScenariosPath | `string` | `/modify-scenarios` | API path for modifying scenarios. `http://localhost:{port}{modifyScenariosPath}` | | resetScenariosPath | `string` | `/reset-scenarios` | API path for resetting scenarios. `http://localhost:{port}{resetScenariosPath}` | +| scenariosPath | `string` | `/scenarios` | API path for getting scenarios. `http://localhost:{port}{scenariosPath}` | | cookieMode | `boolean` | `false` | Whether or not to store scenario selections in a cookie rather than directly in the server | ## Types diff --git a/example/index.ts b/example/index.ts index 6f4612c..34286ff 100644 --- a/example/index.ts +++ b/example/index.ts @@ -156,7 +156,7 @@ run({ }, options: { port: 5000, - uiPath: '/scenarios', + uiPath: '/scenarios-ui', modifyScenariosPath: '/modify', resetScenariosPath: '/reset', }, diff --git a/src/Html.tsx b/src/Html.tsx index 06d5046..cc4e6fd 100644 --- a/src/Html.tsx +++ b/src/Html.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Groups } from './types'; +import { UiGroups } from './types'; function Html({ updatedScenarios, @@ -9,7 +9,7 @@ function Html({ other, }: { uiPath: string; - groups: Groups; + groups: UiGroups; other: Array<{ name: string; checked: boolean }>; updatedScenarios?: string[]; }) { diff --git a/src/apis.ts b/src/apis.ts index b39f6b9..3318034 100644 --- a/src/apis.ts +++ b/src/apis.ts @@ -1,7 +1,8 @@ import { RequestHandler, Response, Request } from 'express'; import { ScenarioMap } from './types'; +import { getScenarios as utilsGetScenarios } from './utils/get-scenarios'; -export { modifyScenarios, resetScenarios }; +export { modifyScenarios, resetScenarios, getScenarios }; function modifyScenarios({ scenarioNames, @@ -60,3 +61,17 @@ function resetScenarios({ res.sendStatus(204); }; } + +function getScenarios({ + scenarioMap, + getScenarioNames, +}: { + scenarioMap: ScenarioMap; + getScenarioNames: (req: Request, res: Response) => string[]; +}): RequestHandler { + return (req: Request, res: Response) => { + const data = utilsGetScenarios(scenarioMap, getScenarioNames(req, res)); + + res.json(data); + }; +} diff --git a/src/index.spec.ts b/src/index.spec.ts index 2c96318..dcbb78e 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1313,7 +1313,7 @@ describe('run', () => { }); }); - it('reset-scenarios and modify-scenarios paths can be changed', async () => { + it('reset-scenarios, modify-scenarios and scenarios paths can be changed', async () => { const initialResponse = { something: 'old' }; const scenarioResponse = { something: 'new' }; const server = run({ @@ -1336,10 +1336,24 @@ describe('run', () => { options: { modifyScenariosPath: '/modify', resetScenariosPath: '/reset', + scenariosPath: '/get-scenarios', }, }); await serverTest(server, async () => { + const scenariosResponse = await rp.get( + 'http://localhost:3000/get-scenarios', + { + json: true, + }, + ); + expect(scenariosResponse).toEqual( + expect.objectContaining({ + groups: expect.anything(), + other: expect.anything(), + }), + ); + const firstResponse = await rp.get('http://localhost:3000/test-me', { json: true, }); @@ -1454,6 +1468,139 @@ describe('run', () => { }); }); }); + + describe('GET scenarios', () => { + it('returns grouped and other scenarios', async () => { + const server = run({ + default: [], + scenarios: { + test1: [ + { + url: '/test-me-1', + method: 'GET', + }, + ], + test2: [ + { + url: '/test-me-2', + method: 'GET', + }, + ], + test3: { + group: 'abc', + mocks: [ + { + url: '/test-me-3', + method: 'GET', + }, + ], + }, + test4: { + group: 'abc', + mocks: [ + { + url: '/test-me-4', + method: 'GET', + }, + ], + }, + }, + }); + + await serverTest(server, async () => { + const scenariosResponse = await rp.get( + 'http://localhost:3000/scenarios', + { + json: true, + }, + ); + + expect(scenariosResponse).toEqual({ + groups: [ + { + name: 'abc', + scenarios: [ + { id: 'test3', selected: false }, + { id: 'test4', selected: false }, + ], + }, + ], + other: [ + { id: 'test1', selected: false }, + { id: 'test2', selected: false }, + ], + }); + }); + }); + + it('returns the correct value for "selected"', async () => { + const server = run({ + default: [], + scenarios: { + test1: [ + { + url: '/test-me-1', + method: 'GET', + }, + ], + test2: [ + { + url: '/test-me-2', + method: 'GET', + }, + ], + test3: { + group: 'abc', + mocks: [ + { + url: '/test-me-3', + method: 'GET', + }, + ], + }, + test4: { + group: 'abc', + mocks: [ + { + url: '/test-me-4', + method: 'GET', + }, + ], + }, + }, + }); + + await serverTest(server, async () => { + await rp.put('http://localhost:3000/modify-scenarios', { + body: { scenarios: ['test2', 'test3'] }, + json: true, + }); + + const scenariosResponse = await rp.get( + 'http://localhost:3000/scenarios', + { + json: true, + }, + ); + + expect(scenariosResponse).toEqual({ + groups: [ + { + name: 'abc', + scenarios: [ + { id: 'test3', selected: true }, + { id: 'test4', selected: false }, + ], + }, + ], + other: [ + { id: 'test1', selected: false }, + { id: 'test2', selected: true }, + ], + }); + }); + }); + }); }); function getStartTime() { diff --git a/src/run.ts b/src/run.ts index 6fe028a..de3b15c 100644 --- a/src/run.ts +++ b/src/run.ts @@ -4,7 +4,11 @@ import express, { Request, Response, NextFunction } from 'express'; import path from 'path'; import { transform } from 'server-with-kill'; -import { modifyScenarios, resetScenarios } from './apis'; +import { + modifyScenarios, + resetScenarios, + getScenarios as apiGetScenarios, +} from './apis'; import { getGraphQlMocks, getGraphQlMock, @@ -70,6 +74,7 @@ function createExpressApp({ uiPath = '/', modifyScenariosPath = '/modify-scenarios', resetScenariosPath = '/reset-scenarios', + scenariosPath = '/scenarios', cookieMode = false, } = options; @@ -134,6 +139,14 @@ function createExpressApp({ }), ); + app.get( + scenariosPath, + apiGetScenarios({ + scenarioMap, + getScenarioNames, + }), + ); + app.use( createRequestHandler({ getScenarioNames, diff --git a/src/types.ts b/src/types.ts index adfbe80..a20b3ec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,6 +87,7 @@ export type Options = { uiPath?: string; modifyScenariosPath?: string; resetScenariosPath?: string; + scenariosPath?: string; cookieMode?: boolean; }; @@ -97,7 +98,7 @@ export type UpdateContext = (partialContext: PartialContext) => Context; export type GetContext = () => Context; -export type Groups = Array<{ +export type UiGroups = Array<{ name: string; noneChecked: boolean; scenarios: Array<{ @@ -106,6 +107,14 @@ export type Groups = Array<{ }>; }>; +export type Groups = Array<{ + name: string; + scenarios: Array<{ + id: string; + selected: boolean; + }>; +}>; + export type CookieValue = { context: Context; scenarios: string[]; diff --git a/src/ui.tsx b/src/ui.tsx index f46e5d0..6b8c070 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { RequestHandler, Request, Response } from 'express'; -import { ScenarioMap, Groups } from './types'; +import { UiGroups, ScenarioMap } from './types'; import { Html } from './Html'; +import { getScenarios } from './utils/get-scenarios'; export { getUi, updateUi }; @@ -82,62 +83,23 @@ function updateUi({ function getPageVariables( scenarioMap: ScenarioMap, selectedScenarios: string[], -) { - const { other, ...groupedScenarios } = Object.entries(scenarioMap).reduce<{ - other: string[]; - [key: string]: string[]; - }>( - (result, [scenarioName, scenarioMock]) => { - if (Array.isArray(scenarioMock) || scenarioMock.group == null) { - result.other.push(scenarioName); - - return result; - } - - const { group } = scenarioMock; - - if (!result[group]) { - result[group] = []; - } - - result[group].push(scenarioName); - - return result; - }, - { other: [] }, - ); - - const groups = Object.entries(groupedScenarios).reduce( - (result, [name, groupScenarios]) => { - let noneChecked = true; - const scenarios = groupScenarios.map(scenario => { - const checked = selectedScenarios.includes(scenario); - if (checked) { - noneChecked = false; - } - - return { - name: scenario, - checked, - }; - }); - - result.push({ - noneChecked, - name, - scenarios, - }); - - return result; - }, - [], - ); +): { groups: UiGroups; other: Array<{ name: string; checked: boolean }> } { + const { groups, other } = getScenarios(scenarioMap, selectedScenarios); return { - groups, - other: other.map(scenario => ({ - name: scenario, - checked: selectedScenarios.includes(scenario), - })), + groups: groups.map(group => { + const scenarios = group.scenarios.map(({ id, selected }) => ({ + name: id, + checked: selected, + })); + const noneChecked = scenarios.every(({ checked }) => !checked); + + return { + name: group.name, + scenarios, + noneChecked, + }; + }), + other: other.map(({ id, selected }) => ({ name: id, checked: selected })), }; } diff --git a/src/utils/get-scenarios.ts b/src/utils/get-scenarios.ts new file mode 100644 index 0000000..66d66c1 --- /dev/null +++ b/src/utils/get-scenarios.ts @@ -0,0 +1,58 @@ +import { Groups, ScenarioMap } from '../types'; + +export { getScenarios }; + +function getScenarios(scenarioMap: ScenarioMap, selectedScenarios: string[]) { + const { other, ...groupedScenarios } = Object.entries(scenarioMap).reduce<{ + other: string[]; + [key: string]: string[]; + }>( + (result, [scenarioName, scenarioMock]) => { + if (Array.isArray(scenarioMock) || scenarioMock.group == null) { + result.other.push(scenarioName); + + return result; + } + + const { group } = scenarioMock; + + if (!result[group]) { + result[group] = []; + } + + result[group].push(scenarioName); + + return result; + }, + { other: [] }, + ); + + const groups = Object.entries(groupedScenarios).reduce( + (result, [name, groupScenarios]) => { + const scenarios = groupScenarios.map(scenarioId => { + const selected = selectedScenarios.includes(scenarioId); + + return { + id: scenarioId, + selected, + }; + }); + + result.push({ + name, + scenarios, + }); + + return result; + }, + [], + ); + + return { + groups, + other: other.map(scenario => ({ + id: scenario, + selected: selectedScenarios.includes(scenario), + })), + }; +}