Skip to content

test: add more tests (next + react) #329

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

Merged
merged 1 commit into from
Aug 22, 2022
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ jobs:
- run: yarn lint
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- run: yarn test-gen && yarn test-app
- run: yarn test-gen
- run: yarn test-next-app
- run: yarn test-react-app
- run: yarn test-gen-env
- run: yarn test-gen-openapi3
- run: yarn check
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@
"eslint-check": "eslint-config-prettier src/index.js",
"build": "babel src -d lib --ignore '*.test.js'",
"watch": "babel --watch src -d lib --ignore '*.test.js'",
"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",
"test-gen": "rm -rf ./tmp && yarn build && ./testgen.sh",
"test-gen-openapi3": "rm -rf ./tmp && yarn build && ENTRYPOINT=https://demo.api-platform.com/docs.json FORMAT=openapi3 ./testgen.sh",
"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",
"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",
"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",
"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'"
"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'",
"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'"
},
"lint-staged": {
"src/**/*.js": "yarn lint --fix"
Expand Down
14 changes: 12 additions & 2 deletions src/generators/NextGenerator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from "chalk";
import handlebars from "handlebars";
import hbh_comparison from "handlebars-helpers/lib/comparison.js";
import hbhComparison from "handlebars-helpers/lib/comparison.js";
import BaseGenerator from "./BaseGenerator.js";

export default class NextGenerator extends BaseGenerator {
Expand Down Expand Up @@ -34,7 +34,7 @@ export default class NextGenerator extends BaseGenerator {
"utils/mercure.ts",
]);

handlebars.registerHelper("compare", hbh_comparison.compare);
handlebars.registerHelper("compare", hbhComparison.compare);
}

help(resource) {
Expand All @@ -59,6 +59,10 @@ export default class NextGenerator extends BaseGenerator {
imports,
hydraPrefix: this.hydraPrefix,
title: resource.title,
hasRelations: fields.some((field) => field.reference || field.embedded),
hasManyRelations: fields.some(
(field) => field.isReferences || field.isEmbeddeds
),
};

// Create directories
Expand Down Expand Up @@ -134,13 +138,19 @@ export default class NextGenerator extends BaseGenerator {
return list;
}

const isReferences = field.reference && field.maxCardinality !== 1;
const isEmbeddeds = field.embedded && field.maxCardinality !== 1;

return {
...list,
[field.name]: {
...field,
type: this.getType(field),
description: this.getDescription(field),
readonly: false,
isReferences,
isEmbeddeds,
isRelations: isEmbeddeds || isReferences,
},
};
}, {});
Expand Down
43 changes: 39 additions & 4 deletions src/generators/ReactGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,17 @@ combineReducers({ ${titleLc},/* ... */ }),

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

const context = {
title: resource.title,
name: resource.name,
lc,
uc: resource.title.toUpperCase(),
fields: resource.readableFields,
ucf,
fields: this.parseFields(resource),
formFields: this.buildFields(resource.writableFields),
hydraPrefix: this.hydraPrefix,
titleUcFirst,
};

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

this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`);
}

getDescription(field) {
return field.description ? field.description.replace(/"/g, "'") : "";
}

parseFields(resource) {
const fields = [
...resource.writableFields,
...resource.readableFields,
].reduce((list, field) => {
if (list[field.name]) {
return list;
}

const isReferences = field.reference && field.maxCardinality !== 1;
const isEmbeddeds = field.embedded && field.maxCardinality !== 1;

return {
...list,
[field.name]: {
...field,
description: this.getDescription(field),
readonly: false,
isReferences,
isEmbeddeds,
isRelations: isEmbeddeds || isReferences,
},
};
}, {});

return fields;
}

ucFirst(target) {
return target.charAt(0).toUpperCase() + target.slice(1);
}
}
4 changes: 1 addition & 3 deletions templates/next/components/common/ReferenceLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ import { Fragment, FunctionComponent } from "react";

interface Props {
items: string | string[];
type: string;
useIcon?: boolean;
}
const ReferenceLinks: FunctionComponent<Props> = ({
items,
type,
useIcon = false,
}) => {
if (Array.isArray(items)) {
return (
<Fragment>
{items.map((item, index) => (
<div key={index}>
<ReferenceLinks items={item} type={type} />
<ReferenceLinks items={item} />
</div>
))}
</Fragment>
Expand Down
101 changes: 74 additions & 27 deletions templates/next/components/foo/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FunctionComponent, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { ErrorMessage, Formik } from "formik";
import { ErrorMessage{{#if hasManyRelations}}, Field, FieldArray{{/if}}, Formik } from "formik";
import { useMutation } from "react-query";

import { fetch, FetchError, FetchResponse } from "../../utils/dataAccess";
Expand Down Expand Up @@ -53,7 +53,20 @@ export const Form: FunctionComponent<Props> = ({ {{{lc}}} }) => {
<div>
<h1>{ {{{lc}}} ? `Edit {{{ucf}}} ${ {{~lc}}['@id']}` : `Create {{{ucf}}}` }</h1>
<Formik
initialValues={ {{~lc}} ? {...{{lc~}} } : new {{{ucf}}}()}
initialValues={
{{lc}} ?
{
...{{lc}},
{{#each fields}}
{{#if isEmbeddeds}}
{{name}}: {{../lc}}.{{name}}?.map((emb: any) => emb['@id']) ?? "",
{{else if embedded}}
{{name}}: {{../lc}}.{{name}}?.['@id'] ?? "",
{{/if}}
{{/each}}
} :
new {{{ucf}}}()
}
validate={() => {
const errors = {};
// add your validation logic here
Expand Down Expand Up @@ -100,32 +113,66 @@ export const Form: FunctionComponent<Props> = ({ {{{lc}}} }) => {
<form onSubmit={handleSubmit}>
{{#each formFields}}
<div className="form-group">
<label className="form-control-label" htmlFor="{{lc}}_{{name}}">{{name}}</label>
<input
name="{{name}}"
id="{{lc}}_{{name}}"
{{#compare type "==" "dateTime" }}
value={values.{{name}}?.toLocaleString() ?? ""}
{{/compare}}
{{#compare type "!=" "dateTime" }}
value={values.{{name}} ?? ""}
{{/compare}}
type="{{type}}"
{{#if step}}step="{{{step}}}"{{/if}}
placeholder="{{{description}}}"
{{#if required}}required={true}{{/if}}
className={`form-control${errors.{{name}} && touched.{{name}} ? ' is-invalid' : ''}`}
aria-invalid={errors.{{name}} && touched.{{name~}} ? 'true' : undefined}
onChange={handleChange}
onBlur={handleBlur}
/>
<ErrorMessage
className="invalid-feedback"
component="div"
name="{{name}}"
/>
{{#if isRelations}}
<div className="form-control-label">{{name}}</div>
<FieldArray
name="{{name}}"
render={(arrayHelpers) => (
<div id="{{../lc}}_{{name}}">
{values.{{name}} && values.{{name}}.length > 0 ? (
values.{{name}}.map((item: any, index: number) => (
<div key={index}>
<Field name={`{{name}}.${index}`} />
<button
type="button"
onClick={() => arrayHelpers.remove(index)}
>
-
</button>
<button
type="button"
onClick={() => arrayHelpers.insert(index, '')}
>
+
</button>
</div>
))
) : (
<button type="button" onClick={() => arrayHelpers.push('')}>
Add
</button>
)}
</div>
)}
/>
{{else}}
<label className="form-control-label" htmlFor="{{../lc}}_{{name}}">{{name}}</label>
<input
name="{{name}}"
id="{{../lc}}_{{name}}"
{{#compare type "==" "dateTime" }}
value={values.{{name}}?.toLocaleString() ?? ""}
{{/compare}}
{{#compare type "!=" "dateTime" }}
value={values.{{name}} ?? ""}
{{/compare}}
type="{{type}}"
{{#if step}}step="{{{step}}}"{{/if}}
placeholder="{{{description}}}"
{{#if required}}required={true}{{/if}}
className={`form-control${errors.{{name}} && touched.{{name}} ? ' is-invalid' : ''}`}
aria-invalid={errors.{{name}} && touched.{{name~}} ? 'true' : undefined}
onChange={handleChange}
onBlur={handleBlur}
/>
<ErrorMessage
className="invalid-feedback"
component="div"
name="{{name}}"
/>
{{/if}}
</div>
{{/each}}
{{/each}}
{status && status.msg && (
<div
className={`alert ${
Expand Down
10 changes: 7 additions & 3 deletions templates/next/components/foo/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,23 @@ export const List: FunctionComponent<Props> = ({ {{{name}}} }) => (
{ {{{name}}} && ({{{name}}}.length !== 0) && {{{name}}}.map( ( {{{lc}}} ) => (
{{{lc}}}['@id'] &&
<tr key={ {{{lc}}}['@id'] }>
<th scope="row"><ReferenceLinks items={ {{{lc}}}['@id'] } type="{{{lc}}}" /></th>
<th scope="row"><ReferenceLinks items={ {{{lc}}}['@id'] } /></th>
{{#each fields}}
<td>
{{#if reference}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } type="{{{reference.title}}}" />
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } />
{{else if isEmbeddeds}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => emb['@id']) } />
{{else if embedded}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}']['@id'] } />
{{else if (compare type "==" "Date") }}
{ {{{../lc}}}['{{{name}}}']?.toLocaleString() }
{{else}}
{ {{{../lc}}}['{{{name}}}'] }
{{/if}}
</td>
{{/each}}
<td><ReferenceLinks items={ {{{lc}}}['@id'] } type="{{{lc}}}" useIcon={true} /></td>
<td><ReferenceLinks items={ {{{lc}}}['@id'] } useIcon={true} /></td>
<td>
<Link href={`${ {{~lc}}["@id"]}/edit`}>
<a>
Expand Down
18 changes: 11 additions & 7 deletions templates/next/components/foo/Show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import Head from "next/head";

{{#if reference}}import ReferenceLinks from "../common/ReferenceLinks";{{/if}}
{{#if hasRelations}}import ReferenceLinks from "../common/ReferenceLinks";{{/if}}
import { fetch } from "../../utils/dataAccess";
import { {{{ucf}}} } from "../../types/{{{ucf}}}";

Expand Down Expand Up @@ -32,9 +32,9 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
return (
<div>
<Head>
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
<script type="application/ld+json" dangerouslySetInnerHTML={ { __html: text } } />
</Head>
<title>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</title>
<script type="application/ld+json" dangerouslySetInnerHTML={ { __html: text } } />
</Head>
<h1>{`Show {{{ucf}}} ${ {{~lc}}['@id']}`}</h1>
<table className="table table-responsive table-striped table-hover">
<thead>
Expand All @@ -49,7 +49,11 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
<th scope="row">{{name}}</th>
<td>
{{#if reference}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } type="{{{reference.title}}}" />
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'] } />
{{else if isEmbeddeds}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}'].map((emb: any) => emb['@id']) } />
{{else if embedded}}
<ReferenceLinks items={ {{{../lc}}}['{{{name}}}']['@id'] } />
{{else if (compare type "==" "Date") }}
{ {{{../lc}}}['{{{name}}}']?.toLocaleString() }
{{else}}
Expand All @@ -68,11 +72,11 @@ export const Show: FunctionComponent<Props> = ({ {{{lc}}}, text }) => {
<Link href="/{{{name}}}">
<a className="btn btn-primary">Back to list</a>
</Link>{" "}
<Link href={`${ {{~lc}}["@id"]}/edit`}>
<Link href={`${ {{~lc}}["@id"]}/edit`}>
<a className="btn btn-warning">Edit</a>
</Link>
<button className="btn btn-danger" onClick={handleDelete}>
<a>Delete</a>
Delete
</button>
</div>
);
Expand Down
23 changes: 1 addition & 22 deletions templates/next/utils/dataAccess.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import get from "lodash/get";
import has from "lodash/has";
import mapValues from "lodash/mapValues";
import isomorphicFetch from "isomorphic-unfetch";

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

export const normalize = (data: any) => {
if (has(data, "{{{hydraPrefix}}}member")) {
// Normalize items in collections
data["{{{hydraPrefix}}}member"] = data[
"{{{hydraPrefix}}}member"
].map((item: unknown) => normalize(item));

return data;
}

// Flatten nested documents
return mapValues(data, (value) =>
Array.isArray(value)
? value.map((v) => get(v, "@id", v))
: get(value, "@id", value)
);
};

export const getPaths = async <TData extends Item>(response: FetchResponse<PagedCollection<TData>> | undefined, resourceName: string, isEdit: boolean) => {
if (!response) return [];

Expand Down
Loading