This commit adds Cypress for testing the library against a real DOM.master
@@ -1,103 +0,0 @@ | |||||
# TSDX User Guide | |||||
Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. | |||||
> This TSDX setup is meant for developing libraries (not apps!) that can be published to NPM. If you’re looking to build a Node app, you could use `ts-node-dev`, plain `ts-node`, or simple `tsc`. | |||||
> If you’re new to TypeScript, checkout [this handy cheatsheet](https://devhints.io/typescript) | |||||
## Commands | |||||
TSDX scaffolds your new library inside `/src`. | |||||
To run TSDX, use: | |||||
```bash | |||||
npm start # or yarn start | |||||
``` | |||||
This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. | |||||
To do a one-off build, use `npm run build` or `yarn build`. | |||||
To run tests, use `npm test` or `yarn test`. | |||||
## Configuration | |||||
Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. | |||||
### Jest | |||||
Jest tests are set up to run with `npm test` or `yarn test`. | |||||
### Bundle Analysis | |||||
[`size-limit`](https://github.com/ai/size-limit) is set up to calculate the real cost of your library with `npm run size` and visualize the bundle with `npm run analyze`. | |||||
#### Setup Files | |||||
This is the folder structure we set up for you: | |||||
```txt | |||||
/src | |||||
index.tsx # EDIT THIS | |||||
/test | |||||
blah.test.tsx # EDIT THIS | |||||
.gitignore | |||||
package.json | |||||
README.md # EDIT THIS | |||||
tsconfig.json | |||||
``` | |||||
### Rollup | |||||
TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. | |||||
### TypeScript | |||||
`tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. | |||||
## Continuous Integration | |||||
### GitHub Actions | |||||
Two actions are added by default: | |||||
- `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix | |||||
- `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) | |||||
## Optimizations | |||||
Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: | |||||
```js | |||||
// ./types/index.d.ts | |||||
declare var __DEV__: boolean; | |||||
// inside your code... | |||||
if (__DEV__) { | |||||
console.log('foo'); | |||||
} | |||||
``` | |||||
You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. | |||||
## Module Formats | |||||
CJS, ESModules, and UMD module formats are supported. | |||||
The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. | |||||
## Named Exports | |||||
Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. | |||||
## Including Styles | |||||
There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. | |||||
For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. | |||||
## Publishing to NPM | |||||
We recommend using [np](https://github.com/sindresorhus/np). |
@@ -0,0 +1,6 @@ | |||||
{ | |||||
"fixturesFolder": "test/fixtures", | |||||
"integrationFolder": "test/integrations", | |||||
"pluginsFile": "test/plugins/index.ts", | |||||
"supportFile": "test/support/index.ts" | |||||
} |
@@ -0,0 +1,8 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"target": "es5", | |||||
"lib": ["es5", "dom"], | |||||
"types": ["cypress"] | |||||
}, | |||||
"include": ["**/*.ts"] | |||||
} |
@@ -15,6 +15,7 @@ | |||||
"build": "tsdx build", | "build": "tsdx build", | ||||
"test": "tsdx test", | "test": "tsdx test", | ||||
"lint": "tsdx lint", | "lint": "tsdx lint", | ||||
"e2e": "cypress open", | |||||
"prepare": "tsdx build", | "prepare": "tsdx build", | ||||
"size": "size-limit", | "size": "size-limit", | ||||
"analyze": "size-limit --why" | "analyze": "size-limit --why" | ||||
@@ -47,6 +48,7 @@ | |||||
"devDependencies": { | "devDependencies": { | ||||
"@size-limit/preset-small-lib": "^4.10.2", | "@size-limit/preset-small-lib": "^4.10.2", | ||||
"@types/jsdom": "^16.2.10", | "@types/jsdom": "^16.2.10", | ||||
"cypress": "^7.2.0", | |||||
"husky": "^6.0.0", | "husky": "^6.0.0", | ||||
"jsdom": "^16.5.3", | "jsdom": "^16.5.3", | ||||
"size-limit": "^4.10.2", | "size-limit": "^4.10.2", | ||||
@@ -1,4 +1,4 @@ | |||||
import * as fixtures from '../test/fixtures' | |||||
import * as fixtures from '../test/utils' | |||||
import getFormValues from '.' | import getFormValues from '.' | ||||
describe('blank template', () => { | describe('blank template', () => { | ||||
@@ -109,20 +109,37 @@ const getFieldValue = (el: FieldNode, submitter?: HTMLSubmitterElement) => { | |||||
const fieldEl = el as HTMLFieldElement | const fieldEl = el as HTMLFieldElement | ||||
const tagName = fieldEl.tagName | const tagName = fieldEl.tagName | ||||
const type = fieldEl.type | const type = fieldEl.type | ||||
if (tagName === 'TEXTAREA') { | |||||
return fieldEl.value.replace(/\n/g, '\r\n') | |||||
} | |||||
if (tagName === 'SELECT' && fieldEl.value === '') { | if (tagName === 'SELECT' && fieldEl.value === '') { | ||||
return null | return null | ||||
} | } | ||||
if (tagName === 'INPUT' && type === 'checkbox') { | |||||
const inputFieldEl = fieldEl as HTMLInputElement | |||||
const checkedValue = inputFieldEl.getAttribute('value') | |||||
if (checkedValue !== null) { | |||||
if (inputFieldEl.checked) { | |||||
return inputFieldEl.value | |||||
if (tagName === 'INPUT') { | |||||
switch (type) { | |||||
case 'checkbox': | |||||
const checkboxEl = fieldEl as HTMLInputElement | |||||
const checkedValue = checkboxEl.getAttribute('value') | |||||
if (checkedValue !== null) { | |||||
if (checkboxEl.checked) { | |||||
return checkboxEl.value | |||||
} | |||||
return null | |||||
} | |||||
return 'on' // default value | |||||
case 'radio': | |||||
const radioEl = fieldEl as HTMLInputElement | |||||
if (radioEl.checked) { | |||||
return radioEl.value | |||||
} | } | ||||
return null | return null | ||||
default: | |||||
break | |||||
} | } | ||||
return inputFieldEl.checked | |||||
} | } | ||||
return fieldEl.value | return fieldEl.value | ||||
@@ -154,15 +171,39 @@ const getFormValues = (form: HTMLFormElement, submitter?: HTMLSubmitterElement) | |||||
const formFieldElements = allFormFieldElements.filter(([, el]) => isValidFormField(el)) | const formFieldElements = allFormFieldElements.filter(([, el]) => isValidFormField(el)) | ||||
const fieldValues = formFieldElements.reduce( | const fieldValues = formFieldElements.reduce( | ||||
(theFormValues, [,el]) => { | (theFormValues, [,el]) => { | ||||
const inputEl = el as HTMLInputElement | |||||
if (inputEl.tagName === 'INPUT' && inputEl.type === 'radio' && !inputEl.checked) { | |||||
return theFormValues | |||||
} | |||||
const fieldValue = getFieldValue(el, submitter) | const fieldValue = getFieldValue(el, submitter) | ||||
if (fieldValue === null) { | if (fieldValue === null) { | ||||
return theFormValues | return theFormValues | ||||
} | } | ||||
return { | |||||
[el['name'] as string]: fieldValue, | |||||
} | |||||
const fieldName = el['name'] as string; | |||||
// const { [fieldName]: oldFormValue = null } = theFormValues; | |||||
// if (oldFormValue === null) { | |||||
return { | |||||
...theFormValues, | |||||
[fieldName]: fieldValue, | |||||
} | |||||
// } | |||||
// | |||||
// if (!Array.isArray(oldFormValue)) { | |||||
// return { | |||||
// ...theFormValues, | |||||
// [fieldName]: [oldFormValue, fieldValue], | |||||
// } | |||||
// } | |||||
// | |||||
// return { | |||||
// ...theFormValues, | |||||
// [fieldName]: [...oldFormValue, fieldValue], | |||||
// } | |||||
}, | }, | ||||
{} | |||||
{} as any | |||||
) | ) | ||||
if (Boolean(submitter as unknown)) { | if (Boolean(submitter as unknown)) { | ||||
return { | return { | ||||
@@ -80,5 +80,19 @@ | |||||
</div> | </div> | ||||
</form> | </form> | ||||
</article> | </article> | ||||
<script> | |||||
Array.from(document.getElementsByClassName('dependents')).forEach(d => { | |||||
d.addEventListener('click', e => { | |||||
const container = document.createElement('div') | |||||
const input = document.createElement('input') | |||||
input.name = 'dependent' | |||||
input.type = 'text' | |||||
input.placeholder = 'Dependent' | |||||
container.classList.add('additional-dependent') | |||||
container.appendChild(input) | |||||
e.target.parentElement.parentElement.insertBefore(container, e.target.parentElement) | |||||
}) | |||||
}) | |||||
</script> | |||||
</body> | </body> | ||||
</html> | </html> |
@@ -0,0 +1,29 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('blank template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/blank.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/blank.html' }).as('submitted'); | |||||
}) | |||||
it('should have blank form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.get('form') | |||||
.then((formResult) => { | |||||
const [form] = Array.from(formResult); | |||||
beforeValues = getFormValues(form); | |||||
form.submit(); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
console.log(beforeValues) | |||||
const before = new URLSearchParams(beforeValues).toString(); | |||||
const after = new URLSearchParams(search).toString(); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}); | |||||
}) |
@@ -0,0 +1,49 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('single input template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/default.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/default.html' }).as('submitted'); | |||||
}) | |||||
it('should have a single form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.then(() => { | |||||
cy.get('[name="first_name"]').type('John') | |||||
cy.get('[name="middle_name"]').type('Marcelo') | |||||
cy.get('[name="last_name"]').type('Dela Cruz') | |||||
cy.get('[name="gender"][value="m"]').check() | |||||
cy.get('[name="civil_status"]').select('Married') | |||||
cy.get('[name="new_registration"]').check() | |||||
cy.get('[name="nationality"][value="filipino"]').check() | |||||
cy.get('[name="dependent"][value="Jun"]').check() | |||||
// cy.get('button.dependents').click() | |||||
// cy.get('.additional-dependent [name="dependent"][type="text"]').eq(0).type('Juana') | |||||
// cy.get('button.dependents').click() | |||||
// cy.get('.additional-dependent [name="dependent"][type="text"]').eq(1).type('Jane') | |||||
// cy.get('button.dependents').click() | |||||
// cy.get('.additional-dependent [name="dependent"][type="text"]').eq(2).type('Josh') | |||||
cy.get('[name="notes"]').type('Test content\n\nNew line\n\nAnother line').as('filled') | |||||
}) | |||||
.get('form') | |||||
.then((theForm) => { | |||||
cy | |||||
.get('[name="submit"][value="Hi"]') | |||||
.then((submitterEl) => { | |||||
const [submitter] = Array.from(submitterEl) as HTMLButtonElement[]; | |||||
beforeValues = getFormValues(theForm[0], submitter); | |||||
submitterEl.trigger('click'); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
const before = JSON.stringify(new URLSearchParams(beforeValues).toString().split('&')); | |||||
const after = JSON.stringify(new URLSearchParams(search).toString().split('&')); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}) | |||||
}) | |||||
}); |
@@ -0,0 +1,28 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('single input template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/single-disabled-input.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-disabled-input.html' }).as('submitted'); | |||||
}) | |||||
it('should have a single form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.get('form') | |||||
.then((formResult) => { | |||||
const [form] = Array.from(formResult); | |||||
beforeValues = getFormValues(form); | |||||
form.submit(); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
const before = new URLSearchParams(beforeValues).toString(); | |||||
const after = new URLSearchParams(search).toString(); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}) | |||||
}); |
@@ -0,0 +1,28 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('single input template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/single-input-with-double-input-submitters.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-input-with-double-input-submitters.html' }).as('submitted'); | |||||
}) | |||||
it('should have a single form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.get('form') | |||||
.then((formResult) => { | |||||
const [form] = Array.from(formResult); | |||||
beforeValues = getFormValues(form); | |||||
form.submit(); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
const before = new URLSearchParams(beforeValues).toString(); | |||||
const after = new URLSearchParams(search).toString(); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}) | |||||
}); |
@@ -0,0 +1,28 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('single input template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/single-input-with-double-submitters.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-input-with-double-submitters.html' }).as('submitted'); | |||||
}) | |||||
it('should have a single form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.get('form') | |||||
.then((formResult) => { | |||||
const [form] = Array.from(formResult); | |||||
beforeValues = getFormValues(form); | |||||
form.submit(); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
const before = new URLSearchParams(beforeValues).toString(); | |||||
const after = new URLSearchParams(search).toString(); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}) | |||||
}); |
@@ -0,0 +1,28 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('single input template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/single-input.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-input.html' }).as('submitted'); | |||||
}) | |||||
it('should have a single form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.get('form') | |||||
.then((formResult) => { | |||||
const [form] = Array.from(formResult); | |||||
beforeValues = getFormValues(form); | |||||
form.submit(); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
const before = new URLSearchParams(beforeValues).toString(); | |||||
const after = new URLSearchParams(search).toString(); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}) | |||||
}); |
@@ -0,0 +1,28 @@ | |||||
/// <reference types="cypress" /> | |||||
import getFormValues from '../../src' | |||||
describe('single input template', () => { | |||||
beforeEach(() => { | |||||
cy.intercept({ url: '/' }, { fixture: 'templates/single-readonly-input.html' }); | |||||
cy.intercept({ url: '/?*' }, { fixture: 'templates/single-readonly-input.html' }).as('submitted'); | |||||
}) | |||||
it('should have a single form value', () => { | |||||
let beforeValues; | |||||
cy | |||||
.visit('/') | |||||
.get('form') | |||||
.then((formResult) => { | |||||
const [form] = Array.from(formResult); | |||||
beforeValues = getFormValues(form); | |||||
form.submit(); | |||||
cy.wait('@submitted') | |||||
cy.location('search').then(search => { | |||||
const before = new URLSearchParams(beforeValues).toString(); | |||||
const after = new URLSearchParams(search).toString(); | |||||
expect(before).to.equal(after); | |||||
}) | |||||
}) | |||||
}) | |||||
}); |
@@ -0,0 +1,22 @@ | |||||
/// <reference types="cypress" /> | |||||
// *********************************************************** | |||||
// This example plugins/index.ts can be used to load plugins | |||||
// | |||||
// You can change the location of this file or turn off loading | |||||
// the plugins file with the 'pluginsFile' configuration option. | |||||
// | |||||
// You can read more here: | |||||
// https://on.cypress.io/plugins-guide | |||||
// *********************************************************** | |||||
// This function is called when a project is opened or re-opened (e.g. due to | |||||
// the project's config changing) | |||||
/** | |||||
* @type {Cypress.PluginConfig} | |||||
*/ | |||||
// eslint-disable-next-line no-unused-vars | |||||
module.exports = (on, config) => { | |||||
// `on` is used to hook into various events Cypress emits | |||||
// `config` is the resolved Cypress config | |||||
} |
@@ -0,0 +1,25 @@ | |||||
// *********************************************** | |||||
// This example commands.js shows you how to | |||||
// create various custom commands and overwrite | |||||
// existing commands. | |||||
// | |||||
// For more comprehensive examples of custom | |||||
// commands please read more here: | |||||
// https://on.cypress.io/custom-commands | |||||
// *********************************************** | |||||
// | |||||
// | |||||
// -- This is a parent command -- | |||||
// Cypress.Commands.add('login', (email, password) => { ... }) | |||||
// | |||||
// | |||||
// -- This is a child command -- | |||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) | |||||
// | |||||
// | |||||
// -- This is a dual command -- | |||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) | |||||
// | |||||
// | |||||
// -- This will overwrite an existing command -- | |||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) |
@@ -0,0 +1,20 @@ | |||||
// *********************************************************** | |||||
// This example support/index.js is processed and | |||||
// loaded automatically before your test files. | |||||
// | |||||
// This is a great place to put global configuration and | |||||
// behavior that modifies Cypress. | |||||
// | |||||
// You can change the location of this file or turn off | |||||
// automatically serving support files with the | |||||
// 'supportFile' configuration option. | |||||
// | |||||
// You can read more here: | |||||
// https://on.cypress.io/configuration | |||||
// *********************************************************** | |||||
// Import commands.ts using ES2015 syntax: | |||||
import './commands' | |||||
// Alternatively you can use CommonJS syntax: | |||||
// require('./commands') |