Skip to content

Commit 4a7571e

Browse files
authored
test: add more tests (next + react) (#329)
1 parent 0a78c78 commit 4a7571e

File tree

21 files changed

+312
-111
lines changed

21 files changed

+312
-111
lines changed

.github/workflows/ci.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ jobs:
2424
- run: yarn lint
2525
- name: Install Playwright Browsers
2626
run: yarn playwright install --with-deps
27-
- run: yarn test-gen && yarn test-app
27+
- run: yarn test-gen
28+
- run: yarn test-next-app
29+
- run: yarn test-react-app
2830
- run: yarn test-gen-env
2931
- run: yarn test-gen-openapi3
3032
- run: yarn check

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@
5959
"eslint-check": "eslint-config-prettier src/index.js",
6060
"build": "babel src -d lib --ignore '*.test.js'",
6161
"watch": "babel --watch src -d lib --ignore '*.test.js'",
62-
"test-gen": "rm -rf ./tmp && yarn build && ./lib/index.js https://demo.api-platform.com ./tmp/react -g react && ./lib/index.js https://demo.api-platform.com ./tmp/react-native -g react-native && ./lib/index.js https://demo.api-platform.com ./tmp/next -g next && ./lib/index.js https://demo.api-platform.com ./tmp/vue -g vue",
62+
"test-gen": "rm -rf ./tmp && yarn build && ./testgen.sh",
63+
"test-gen-openapi3": "rm -rf ./tmp && yarn build && ENTRYPOINT=https://demo.api-platform.com/docs.json FORMAT=openapi3 ./testgen.sh",
6364
"test-gen-custom": "rm -rf ./tmp && yarn build && babel src/generators/ReactGenerator.js src/generators/BaseGenerator.js -d ./tmp/gens && cp -r ./templates/react ./templates/react-common ./templates/entrypoint.js ./tmp/gens && ./lib/index.js https://demo.api-platform.com ./tmp/react-custom -g \"$(pwd)/tmp/gens/ReactGenerator.js\" -t ./tmp/gens",
64-
"test-gen-openapi3": "rm -rf ./tmp && yarn build && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/react -f openapi3 && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/react-native -g react-native -f openapi3 && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/vue -g vue -f openapi3",
6565
"test-gen-env": "rm -rf ./tmp && yarn build && API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=https://demo.api-platform.com API_PLATFORM_CLIENT_GENERATOR_OUTPUT=./tmp ./lib/index.js",
66-
"test-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create next-app --typescript ./tmp/app/next && yarn --cwd ./tmp/app/next add lodash.get lodash.has isomorphic-unfetch formik react-query && yarn --cwd ./tmp/app/next add -D @types/lodash && cp -R ./tmp/next/* ./tmp/app/next && rm ./tmp/app/next/pages/index.tsx && rm -rf ./tmp/app/next/pages/api && yarn --cwd ./tmp/app/next build && start-server-and-test 'yarn --cwd ./tmp/app/next start' http://127.0.0.1:3000/books 'yarn playwright test'"
66+
"test-react-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create react-app ./tmp/app/reactapp && yarn --cwd ./tmp/app/reactapp add react-router-dom@5 redux redux-thunk react-redux redux-form connected-react-router && cp -R ./tmp/react/* ./tmp/app/reactapp/src && cp ./templates/react/index.js ./tmp/app/reactapp/src && start-server-and-test 'BROWSER=none yarn --cwd ./tmp/app/reactapp start' http://127.0.0.1:3000/books/ 'yarn playwright test'",
67+
"test-next-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create next-app --typescript ./tmp/app/next && yarn --cwd ./tmp/app/next add isomorphic-unfetch formik react-query && cp -R ./tmp/next/* ./tmp/app/next && rm ./tmp/app/next/pages/index.tsx && rm -rf ./tmp/app/next/pages/api && yarn --cwd ./tmp/app/next build && start-server-and-test 'yarn --cwd ./tmp/app/next start' http://127.0.0.1:3000/books/ 'yarn playwright test'"
6768
},
6869
"lint-staged": {
6970
"src/**/*.js": "yarn lint --fix"

src/generators/NextGenerator.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import chalk from "chalk";
22
import handlebars from "handlebars";
3-
import hbh_comparison from "handlebars-helpers/lib/comparison.js";
3+
import hbhComparison from "handlebars-helpers/lib/comparison.js";
44
import BaseGenerator from "./BaseGenerator.js";
55

66
export default class NextGenerator extends BaseGenerator {
@@ -34,7 +34,7 @@ export default class NextGenerator extends BaseGenerator {
3434
"utils/mercure.ts",
3535
]);
3636

37-
handlebars.registerHelper("compare", hbh_comparison.compare);
37+
handlebars.registerHelper("compare", hbhComparison.compare);
3838
}
3939

4040
help(resource) {
@@ -59,6 +59,10 @@ export default class NextGenerator extends BaseGenerator {
5959
imports,
6060
hydraPrefix: this.hydraPrefix,
6161
title: resource.title,
62+
hasRelations: fields.some((field) => field.reference || field.embedded),
63+
hasManyRelations: fields.some(
64+
(field) => field.isReferences || field.isEmbeddeds
65+
),
6266
};
6367

6468
// Create directories
@@ -134,13 +138,19 @@ export default class NextGenerator extends BaseGenerator {
134138
return list;
135139
}
136140

141+
const isReferences = field.reference && field.maxCardinality !== 1;
142+
const isEmbeddeds = field.embedded && field.maxCardinality !== 1;
143+
137144
return {
138145
...list,
139146
[field.name]: {
140147
...field,
141148
type: this.getType(field),
142149
description: this.getDescription(field),
143150
readonly: false,
151+
isReferences,
152+
isEmbeddeds,
153+
isRelations: isEmbeddeds || isReferences,
144154
},
145155
};
146156
}, {});

src/generators/ReactGenerator.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,17 @@ combineReducers({ ${titleLc},/* ... */ }),
6868

6969
generate(api, resource, dir) {
7070
const lc = resource.title.toLowerCase();
71-
const titleUcFirst =
72-
resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
71+
const ucf = this.ucFirst(resource.title);
7372

7473
const context = {
7574
title: resource.title,
7675
name: resource.name,
7776
lc,
7877
uc: resource.title.toUpperCase(),
79-
fields: resource.readableFields,
78+
ucf,
79+
fields: this.parseFields(resource),
8080
formFields: this.buildFields(resource.writableFields),
8181
hydraPrefix: this.hydraPrefix,
82-
titleUcFirst,
8382
};
8483

8584
// Create directories
@@ -134,4 +133,40 @@ combineReducers({ ${titleLc},/* ... */ }),
134133

135134
this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`);
136135
}
136+
137+
getDescription(field) {
138+
return field.description ? field.description.replace(/"/g, "'") : "";
139+
}
140+
141+
parseFields(resource) {
142+
const fields = [
143+
...resource.writableFields,
144+
...resource.readableFields,
145+
].reduce((list, field) => {
146+
if (list[field.name]) {
147+
return list;
148+
}
149+
150+
const isReferences = field.reference && field.maxCardinality !== 1;
151+
const isEmbeddeds = field.embedded && field.maxCardinality !== 1;
152+
153+
return {
154+
...list,
155+
[field.name]: {
156+
...field,
157+
description: this.getDescription(field),
158+
readonly: false,
159+
isReferences,
160+
isEmbeddeds,
161+
isRelations: isEmbeddeds || isReferences,
162+
},
163+
};
164+
}, {});
165+
166+
return fields;
167+
}
168+
169+
ucFirst(target) {
170+
return target.charAt(0).toUpperCase() + target.slice(1);
171+
}
137172
}

templates/next/components/common/ReferenceLinks.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ import { Fragment, FunctionComponent } from "react";
33

44
interface Props {
55
items: string | string[];
6-
type: string;
76
useIcon?: boolean;
87
}
98
const ReferenceLinks: FunctionComponent<Props> = ({
109
items,
11-
type,
1210
useIcon = false,
1311
}) => {
1412
if (Array.isArray(items)) {
1513
return (
1614
<Fragment>
1715
{items.map((item, index) => (
1816
<div key={index}>
19-
<ReferenceLinks items={item} type={type} />
17+
<ReferenceLinks items={item} />
2018
</div>
2119
))}
2220
</Fragment>

templates/next/components/foo/Form.tsx

+74-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FunctionComponent, useState } from "react";
22
import Link from "next/link";
33
import { useRouter } from "next/router";
4-
import { ErrorMessage, Formik } from "formik";
4+
import { ErrorMessage{{#if hasManyRelations}}, Field, FieldArray{{/if}}, Formik } from "formik";
55
import { useMutation } from "react-query";
66

77
import { fetch, FetchError, FetchResponse } from "../../utils/dataAccess";
@@ -53,7 +53,20 @@ export const Form: FunctionComponent<Props> = ({ {{{lc}}} }) => {
5353
<div>
5454
<h1>{ {{{lc}}} ? `Edit {{{ucf}}} ${ {{~lc}}['@id']}` : `Create {{{ucf}}}` }</h1>
5555
<Formik
56-
initialValues={ {{~lc}} ? {...{{lc~}} } : new {{{ucf}}}()}
56+
initialValues={
57+
{{lc}} ?
58+
{
59+
...{{lc}},
60+
{{#each fields}}
61+
{{#if isEmbeddeds}}
62+
{{name}}: {{../lc}}.{{name}}?.map((emb: any) => emb['@id']) ?? "",
63+
{{else if embedded}}
64+
{{name}}: {{../lc}}.{{name}}?.['@id'] ?? "",
65+
{{/if}}
66+
{{/each}}
67+
} :
68+
new {{{ucf}}}()
69+
}
5770
validate={() => {
5871
const errors = {};
5972
// add your validation logic here
@@ -100,32 +113,66 @@ export const Form: FunctionComponent<Props> = ({ {{{lc}}} }) => {
100113
<form onSubmit={handleSubmit}>
101114
{{#each formFields}}
102115
<div className="form-group">
103-
<label className="form-control-label" htmlFor="{{lc}}_{{name}}">{{name}}</label>
104-
<input
105-
name="{{name}}"
106-
id="{{lc}}_{{name}}"
107-
{{#compare type "==" "dateTime" }}
108-
value={values.{{name}}?.toLocaleString() ?? ""}
109-
{{/compare}}
110-
{{#compare type "!=" "dateTime" }}
111-
value={values.{{name}} ?? ""}
112-
{{/compare}}
113-
type="{{type}}"
114-
{{#if step}}step="{{{step}}}"{{/if}}
115-
placeholder="{{{description}}}"
116-
{{#if required}}required={true}{{/if}}
117-
className={`form-control${errors.{{name}} && touched.{{name}} ? ' is-invalid' : ''}`}
118-
aria-invalid={errors.{{name}} && touched.{{name~}} ? 'true' : undefined}
119-
onChange={handleChange}
120-
onBlur={handleBlur}
121-
/>
122-
<ErrorMessage
123-
className="invalid-feedback"
124-
component="div"
125-
name="{{name}}"
126-
/>
116+
{{#if isRelations}}
117+
<div className="form-control-label">{{name}}</div>
118+
<FieldArray
119+
name="{{name}}"
120+
render={(arrayHelpers) => (
121+
<div id="{{../lc}}_{{name}}">
122+
{values.{{name}} && values.{{name}}.length > 0 ? (
123+
values.{{name}}.map((item: any, index: number) => (
124+
<div key={index}>
125+
<Field name={`{{name}}.${index}`} />
126+
<button
127+
type="button"
128+
onClick={() => arrayHelpers.remove(index)}
129+
>
130+
-
131+
</button>
132+
<button
133+
type="button"
134+
onClick={() => arrayHelpers.insert(index, '')}
135+
>
136+
+
137+
</button>
138+
</div>
139+
))
140+
) : (
141+
<button type="button" onClick={() => arrayHelpers.push('')}>
142+
Add
143+
</button>
144+
)}
145+
</div>
146+
)}
147+
/>
148+
{{else}}
149+
<label className="form-control-label" htmlFor="{{../lc}}_{{name}}">{{name}}</label>
150+
<input
151+
name="{{name}}"
152+
id="{{../lc}}_{{name}}"
153+
{{#compare type "==" "dateTime" }}
154+
value={values.{{name}}?.toLocaleString() ?? ""}
155+
{{/compare}}
156+
{{#compare type "!=" "dateTime" }}
157+
value={values.{{name}} ?? ""}
158+
{{/compare}}
159+
type="{{type}}"
160+
{{#if step}}step="{{{step}}}"{{/if}}
161+
placeholder="{{{description}}}"
162+
{{#if required}}required={true}{{/if}}
163+
className={`form-control${errors.{{name}} && touched.{{name}} ? ' is-invalid' : ''}`}
164+
aria-invalid={errors.{{name}} && touched.{{name~}} ? 'true' : undefined}
165+
onChange={handleChange}
166+
onBlur={handleBlur}
167+
/>
168+
<ErrorMessage
169+
className="invalid-feedback"
170+
component="div"
171+
name="{{name}}"
172+
/>
173+
{{/if}}
127174
</div>
128-
{{/each}}
175+
{{/each}}
129176
{status && status.msg && (
130177
<div
131178
className={`alert ${

templates/next/components/foo/List.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,23 @@ export const List: FunctionComponent<Props> = ({ {{{name}}} }) => (
2828
{ {{{name}}} && ({{{name}}}.length !== 0) && {{{name}}}.map( ( {{{lc}}} ) => (
2929
{{{lc}}}['@id'] &&
3030
<tr key={ {{{lc}}}['@id'] }>
31-
<th scope="row"><ReferenceLinks items={ {{{lc}}}['@id'] } type="{{{lc}}}" /></th>
31+
<th scope="row"><ReferenceLinks items={ {{{lc}}}['@id'] } /></th>
3232
{{#each fields}}
3333
<td>
3434
{{#if reference}}
35-
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } type="{{{reference.title}}}" />
35+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } />
36+
{{else if isEmbeddeds}}
37+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => emb['@id']) } />
38+
{{else if embedded}}
39+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}']['@id'] } />
3640
{{else if (compare type "==" "Date") }}
3741
{ {{{../lc}}}['{{{name}}}']?.toLocaleString() }
3842
{{else}}
3943
{ {{{../lc}}}['{{{name}}}'] }
4044
{{/if}}
4145
</td>
4246
{{/each}}
43-
<td><ReferenceLinks items={ {{{lc}}}['@id'] } type="{{{lc}}}" useIcon={true} /></td>
47+
<td><ReferenceLinks items={ {{{lc}}}['@id'] } useIcon={true} /></td>
4448
<td>
4549
<Link href={`${ {{~lc}}["@id"]}/edit`}>
4650
<a>

templates/next/components/foo/Show.tsx

+11-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Link from "next/link";
33
import { useRouter } from "next/router";
44
import Head from "next/head";
55

6-
{{#if reference}}import ReferenceLinks from "../common/ReferenceLinks";{{/if}}
6+
{{#if hasRelations}}import ReferenceLinks from "../common/ReferenceLinks";{{/if}}
77
import { fetch } from "../../utils/dataAccess";
88
import { {{{ucf}}} } from "../../types/{{{ucf}}}";
99

@@ -32,9 +32,9 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
3232
return (
3333
<div>
3434
<Head>
35-
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
36-
<script type="application/ld+json" dangerouslySetInnerHTML={ { __html: text } } />
37-
</Head>
35+
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
36+
<script type="application/ld+json" dangerouslySetInnerHTML={ { __html: text } } />
37+
</Head>
3838
<h1>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</h1>
3939
<table className="table table-responsive table-striped table-hover">
4040
<thead>
@@ -49,7 +49,11 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
4949
<th scope="row">{{name}}</th>
5050
<td>
5151
{{#if reference}}
52-
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } type="{{{reference.title}}}" />
52+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } />
53+
{{else if isEmbeddeds}}
54+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => emb['@id']) } />
55+
{{else if embedded}}
56+
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}']['@id'] } />
5357
{{else if (compare type "==" "Date") }}
5458
{ {{{../lc}}}['{{{name}}}']?.toLocaleString() }
5559
{{else}}
@@ -68,11 +72,11 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
6872
<Link href="/{{{name}}}">
6973
<a className="btn btn-primary">Back to list</a>
7074
</Link>{" "}
71-
<Link href={`${ {{~lc}}["@id"]}/edit`}>
75+
<Link href={`${ {{~lc}}["@id"]}/edit`}>
7276
<a className="btn btn-warning">Edit</a>
7377
</Link>
7478
<button className="btn btn-danger" onClick={handleDelete}>
75-
<a>Delete</a>
79+
Delete
7680
</button>
7781
</div>
7882
);

templates/next/utils/dataAccess.ts

+1-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import get from "lodash/get";
2-
import has from "lodash/has";
3-
import mapValues from "lodash/mapValues";
41
import isomorphicFetch from "isomorphic-unfetch";
52

63
import { PagedCollection } from "../types/collection";
@@ -56,7 +53,7 @@ export const fetch = async <TData>(id: string, init: RequestInit = {}): Promise<
5653
if (resp.ok) {
5754
return {
5855
hubURL: extractHubURL(resp)?.toString() || null, // URL cannot be serialized as JSON, must be sent as string
59-
data: normalize(json),
56+
data: json,
6057
text,
6158
};
6259
}
@@ -73,24 +70,6 @@ export const fetch = async <TData>(id: string, init: RequestInit = {}): Promise<
7370
throw { message: errorMessage, status, fields } as FetchError;
7471
};
7572

76-
export const normalize = (data: any) => {
77-
if (has(data, "{{{hydraPrefix}}}member")) {
78-
// Normalize items in collections
79-
data["{{{hydraPrefix}}}member"] = data[
80-
"{{{hydraPrefix}}}member"
81-
].map((item: unknown) => normalize(item));
82-
83-
return data;
84-
}
85-
86-
// Flatten nested documents
87-
return mapValues(data, (value) =>
88-
Array.isArray(value)
89-
? value.map((v) => get(v, "@id", v))
90-
: get(value, "@id", value)
91-
);
92-
};
93-
9473
export const getPaths = async <TData extends Item>(response: FetchResponse<PagedCollection<TData>> | undefined, resourceName: string, isEdit: boolean) => {
9574
if (!response) return [];
9675

0 commit comments

Comments
 (0)