Skip to content

Commit

Permalink
Merge pull request #1632 from SUI-Components/adr-mother-object-msw
Browse files Browse the repository at this point in the history
[ADR] Mother Objects
  • Loading branch information
carlosvillu authored Feb 7, 2024
2 parents 2e7fd86 + 1f0dab0 commit 53df54f
Show file tree
Hide file tree
Showing 10 changed files with 472 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/sui-bundler/shared/define.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ if (process.platform === 'win32') {

module.exports = (vars = {}) =>
new webpack.DefinePlugin({
__MOCKS_API_PATH__: JSON.stringify(process.env.MOCKS_API_PATH || process.env.PWD + '/mocks/routes'),
__DEV__: false,
__BASE_DIR__: JSON.stringify(process.env.PWD),
...vars
Expand Down
43 changes: 43 additions & 0 deletions packages/sui-mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,49 @@ import {getUserHandler} from './mocks/exampleGateway/user/handlers.js'
export default [getUserHandler]
```

### 1.1 Autoload mocks folder.

Any file, under the folder `routes` will be autoloaded and you dont have to do anything to see your handler capturing request.

When you use the autoload feature, you have to create a folder structure following the same structure than your API request.

For example, to capture the request `https://api.site.com/api/v1/user/123/settings`, your folder structure in your project will be:

```
[root mono-repo]
|
- mocks
|
- routes
|
- api
|
- v1
|
- user
|
- :id
|
- settings
|
- index.js
```

Your `index.js` file has to expose a function for each http method that you want to capture:

```js
export async function get({headers, body, params, query, cookies}) {
return [200, {name: 'nombre'}]
}
export async function post({headers, body, params, query, cookies}) {
return [200, {params}]
}
export async function put() {}
export async function del() {}
```

Every function have to return the status code of the request and the body.

### 2. Expose mocker from mocks folder

Once we have the handlers created, we will need to create a mocker with **all the handlers already defined**.
Expand Down
50 changes: 49 additions & 1 deletion packages/sui-mock/src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
/* global __MOCKS_API_PATH__ */
import {rest} from 'msw'

import {getBrowserMocker} from './browser.js'
import {getServerMocker} from './server.js'

const isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'

const setupMocker = isNode ? getServerMocker : getBrowserMocker
const generateHandlerFromContext = requester => {
const handlers = requester
.keys()
.filter(path => path.startsWith('./'))
.map(key => {
const module = requester(key)
return {
path: key,
...(module.get && {get: module.get}),
...(module.post && {post: module.post}),
...(module.put && {put: module.put}),
...(module.del && {delete: module.del}),
...(module.patch && {patch: module.patch})
}
})
.map(descriptor => {
const {path, ...handlers} = descriptor
const url = path.replace('./', 'https://').replace('/index.js', '')
return Object.entries(handlers).map(([method, handler]) => {
return rest[method](url, async (req, res, ctx) => {
const body = ['POST', 'PATCH'].includes(req.method) ? await req.json() : '' // eslint-disable-line
const [status, json] = await handler({
headers: req.headers.all(),
params: req.params,
query: Object.fromEntries(req.url.searchParams),
cookies: req.cookies,
body
})
return res(ctx.status(status), ctx.json(json))
})
})
})
.flat(Infinity)
return handlers
}

const setupMocker = legacyHandlers => {
const mocker = isNode ? getServerMocker : getBrowserMocker
let apiContextRequest
try {
apiContextRequest = require.context(__MOCKS_API_PATH__, true, /index\.js$/)
} catch (err) {
console.error(`[sui-mock] Not found route folder in ${__MOCKS_API_PATH__} autoload of msw handlers disabled`)
apiContextRequest = false
}

return mocker([...legacyHandlers, ...(apiContextRequest && generateHandlerFromContext(apiContextRequest))])
}

export {setupMocker, rest}
4 changes: 2 additions & 2 deletions packages/sui-react-initial-props/src/loadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import withInitialProps from './withInitialProps'

const EMPTY_GET_INITIAL_PROPS = async (): Promise<object> => ({})

const createUniversalPage = (routeInfo: ReactRouterTypes.RouteInfo) => ({ default: Page }: {default: ClientPageComponent}) => {
const createUniversalPage = (routeInfo: ReactRouterTypes.RouteInfo) => async ({ default: Page }: {default: ClientPageComponent}) => {
// check if the Page page has a getInitialProps, if not put a resolve with an empty object
Page.getInitialProps =
typeof Page.getInitialProps === 'function'
Expand All @@ -17,7 +17,7 @@ const createUniversalPage = (routeInfo: ReactRouterTypes.RouteInfo) => ({ defaul
// CLIENT
if (typeof window !== 'undefined') {
// let withInitialProps HOC handle client getInitialProps logic
return Promise.resolve(withInitialProps(Page))
return await Promise.resolve(withInitialProps(Page))
}
// SERVER
// Create a component that gets the initialProps from context
Expand Down
14 changes: 8 additions & 6 deletions packages/sui-test/bin/karma/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,19 @@ const config = {
}
},
plugins: [
new webpack.DefinePlugin({
__BASE_DIR__: JSON.stringify(process.env.PWD),
PATTERN: JSON.stringify(process.env.PATTERN),
CATEGORIES: JSON.stringify(process.env.CATEGORIES)
new webpack.ProvidePlugin({
process: require.resolve('process/browser')
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
...environmentVariables
}),
new webpack.ProvidePlugin({
process: require.resolve('process/browser')
new webpack.DefinePlugin({
__MOCKS_API_PATH__: JSON.stringify(process.env.MOCKS_API_PATH || process.env.PWD + '/mocks/routes'),
'process.env.SEED': JSON.stringify(process.env.SEED),
__BASE_DIR__: JSON.stringify(process.env.PWD),
PATTERN: JSON.stringify(process.env.PATTERN),
CATEGORIES: JSON.stringify(process.env.CATEGORIES)
})
],
module: {
Expand Down
3 changes: 3 additions & 0 deletions packages/sui-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@
"@babel/core": "7.18.10",
"@babel/plugin-transform-modules-commonjs": "7.18.6",
"@babel/register": "7.18.9",
"@faker-js/faker": "8.0.2",
"@s-ui/helpers": "1",
"babel-loader": "8.3.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"babel-plugin-istanbul": "6.0.0",
"babel-preset-sui": "3",
"chai": "3.5.0",
"commander": "8.3.0",
"diff": "5.1.0",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-coverage": "2.2.1",
Expand Down
107 changes: 107 additions & 0 deletions packages/sui-test/src/MotherObject/FakeGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {faker} from '@faker-js/faker'

const IDENTITY_FN = i => i

let instanceRndID
export class RandomID {
static create() {
if (instanceRndID) return instanceRndID
const seed = process.env.SEED || Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
instanceRndID = new RandomID(seed)

console.log(`[RandomID.create] Faker created with seed: ${seed}`)
return instanceRndID
}

static restore() {
instanceRndID = undefined
}

constructor(id) {
this._id = id
}

get id() {
return this._id
}
}

export class FakeGenerator {
static create() {
const seed = RandomID.create().id

return new FakeGenerator(seed)
}

constructor(seed) {
faker.seed(seed)
this._faker = faker
}

city() {
return this._faker.location.city()
}

email() {
return this._faker.internet.email()
}

uuid() {
return this._faker.string.uuid()
}

words({count, replacer = IDENTITY_FN}) {
return replacer(this._faker.lorem.words(count))
}

color() {
return this._faker.color.rgb({format: 'hex', casing: 'lower'})
}

date({from, to} = {}) {
return this._faker.date.between({from, to})
}

pick(list) {
return list[Math.floor(Math.random() * list.length)]
}

bool() {
return Math.random() < 0.5
}

province() {
return this._faker.location.county()
}

phone() {
return this._faker.phone.number()
}

URL() {
return this._faker.internet.url()
}

imgURL({removeHost} = {removeHost: true}) {
const url = this._faker.image.imageUrl()

if (!removeHost) return url

const uri = new URL(url)
return uri.pathname.replace('/', '')
}

number({min = 0, max = Infinity}) {
const difference = max - min

let rand = Math.random()
rand = Math.floor(rand * difference)
rand = rand + min

return rand
}

zipCode() {
return this._faker.location.zipCode()
}
}
76 changes: 76 additions & 0 deletions packages/sui-test/src/MotherObject/Fetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
let _instance
export class MockFetcherManager {
_fakeRequests
_methods = ['get', 'patch', 'post']

static create(FetchFetcher, {InlineError}) {
if (_instance) return _instance
_instance = new MockFetcherManager(FetchFetcher, {InlineError})
return _instance
}

static restore() {
if (!_instance) return console.warn('Unable to restore a non-initialized MockFetcherManager')
_instance.restore()
_instance = undefined
}

constructor(FetchFetcher, {InlineError}) {
this.FetchFetcher = FetchFetcher
this._originalsFetcher = this._methods.map(method => this.FetchFetcher.prototype[method])
this._fakeRequests = {}
this._hasInlineErrors = InlineError
this._methods.forEach(method => {
this.FetchFetcher.prototype[method] = this._request(method)
})
}

addMock({url, method, error, mock, force}) {
const key = method.toUpperCase() + '::' + url
if (!force && this._fakeRequests[key])
throw new Error(`[MockFetcherManager#addMock] forbidden overwrite the request ${key}`)

this._fakeRequests[key] = [error, mock]
}

validate({url, method}) {
const key = method.toUpperCase() + '::' + url
if (this._fakeRequests[key]) throw new Error(`[MockFetcherManager#validate] request ${key} don't consume`)
}

restore() {
if (Object.keys(this._fakeRequests).length === 0) {
this._methods.forEach((method, index) => (this.FetchFetcher.prototype[method] = this._originalsFetcher[index]))
} else {
throw new Error(`[MockFetcherManager#restore]
Unabled restore the FetchFetcher because there are request w/out been consume
- Dont match any mock:
${Object.keys(this._fakeRequests).join('\n\t\t')}
`)
}
}

_request(method) {
const self = this
return function (...args) {
const [url] = args
const requestKey = method.toUpperCase() + '::' + url
// this === FetchFetcher instance
if (self._fakeRequests[requestKey]) {
const [error, response] = self._fakeRequests[requestKey]
delete self._fakeRequests[requestKey]
if (!self._hasInlineErrors) return error ? Promise.reject(error) : Promise.resolve({data: response})

return Promise.resolve([error, response])
} else {
console.warn(`[MockFetcherManager#_request]
- Request make ${requestKey}
- Dont match any mock:
${Object.keys(self._fakeRequests).join('\n\t\t')}
`)
}

return this._originalsFetcher[method].apply(this, args)
}
}
}
Loading

0 comments on commit 53df54f

Please sign in to comment.