diff --git a/packages/app-web/cypress.json b/packages/app-web/cypress.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/packages/app-web/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/app-web/cypress/integration/login.ts b/packages/app-web/cypress/integration/login.ts new file mode 100644 index 0000000..0ed8bc3 --- /dev/null +++ b/packages/app-web/cypress/integration/login.ts @@ -0,0 +1,5 @@ +describe('login', () => { + it('should log the current user in', () => { + cy.visit('http://localhost:3000') + }) +}) diff --git a/packages/app-web/cypress/plugins/index.ts b/packages/app-web/cypress/plugins/index.ts new file mode 100644 index 0000000..1b63c6e --- /dev/null +++ b/packages/app-web/cypress/plugins/index.ts @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js 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 +export default (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/packages/app-web/cypress/support/commands.ts b/packages/app-web/cypress/support/commands.ts new file mode 100644 index 0000000..119ab03 --- /dev/null +++ b/packages/app-web/cypress/support/commands.ts @@ -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) => { ... }) diff --git a/packages/app-web/cypress/support/index.ts b/packages/app-web/cypress/support/index.ts new file mode 100644 index 0000000..d68db96 --- /dev/null +++ b/packages/app-web/cypress/support/index.ts @@ -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.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/packages/app-web/cypress/tsconfig.json b/packages/app-web/cypress/tsconfig.json new file mode 100644 index 0000000..20dff1e --- /dev/null +++ b/packages/app-web/cypress/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + }, + "include": ["**/*.ts"] +} diff --git a/packages/app-web/jest.config.js b/packages/app-web/jest.config.js index 4daac1a..cc0c90b 100644 --- a/packages/app-web/jest.config.js +++ b/packages/app-web/jest.config.js @@ -8,5 +8,7 @@ module.exports = { }, collectCoverageFrom: [ 'src/components/**/*.{jsx,tsx}', + 'src/modules/**/*.{js,ts}', + 'src/utils/**/*.{js,ts}', ] }; diff --git a/packages/app-web/next-env.d.ts b/packages/app-web/next-env.d.ts index 7b7aa2c..2a9771c 100644 --- a/packages/app-web/next-env.d.ts +++ b/packages/app-web/next-env.d.ts @@ -1,2 +1,3 @@ /// /// +/// diff --git a/packages/app-web/package.json b/packages/app-web/package.json index b016ce6..f805939 100644 --- a/packages/app-web/package.json +++ b/packages/app-web/package.json @@ -6,10 +6,12 @@ "dev": "next dev", "build": "next build", "start": "next start", - "test": "jest" + "test": "jest", + "e2e": "cypress" }, "dependencies": { "@tesseract-design/viewfinder": "^0.1.1", + "@theoryofnekomata/formxtr": "^0.1.2", "next": "10.2.0", "react": "17.0.2", "react-dom": "17.0.2", @@ -21,6 +23,7 @@ "@types/node": "^15.0.2", "@types/react": "^17.0.5", "@types/styled-components": "^5.1.9", + "cypress": "^7.3.0", "jest": "^26.6.3", "ts-jest": "^26.5.6", "typescript": "^4.2.4" diff --git a/packages/app-web/src/components/molecules/forms/ActionButton/index.test.tsx b/packages/app-web/src/components/molecules/forms/ActionButton/index.test.tsx new file mode 100644 index 0000000..993556e --- /dev/null +++ b/packages/app-web/src/components/molecules/forms/ActionButton/index.test.tsx @@ -0,0 +1,22 @@ +import {cleanup, render} from '@testing-library/react' +import ActionButton from '.' + +describe('button component for triggering actions', () => { + afterEach(() => { + cleanup() + }) + + it('should render a button element with a no-op action', () => { + const result = render() + const input = result.queryByRole('button') + expect(input).not.toBeNull() + }) + + describe.each(['button', 'reset', 'submit'] as const)('on %p action', (type) => { + it('should render a button element with a submit action', () => { + const result = render() + const input = result.queryByRole('button') as HTMLButtonElement + expect(input.type).toBe(type) + }) + }) +}) diff --git a/packages/app-web/src/components/molecules/forms/ActionButton/index.tsx b/packages/app-web/src/components/molecules/forms/ActionButton/index.tsx new file mode 100644 index 0000000..cedb38d --- /dev/null +++ b/packages/app-web/src/components/molecules/forms/ActionButton/index.tsx @@ -0,0 +1,89 @@ +import styled from 'styled-components'; +import {FC, ReactChild} from 'react'; + +const Base = styled('div')({ + height: '3rem', + borderRadius: '0.25rem', + overflow: 'hidden', + position: 'relative', + '::before': { + content: "''", + borderWidth: 1, + borderStyle: 'solid', + borderColor: 'inherit', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + borderRadius: 'inherit', + boxSizing: 'border-box', + }, +}) + +const ClickArea = styled('button')({ + display: 'block', + width: '100%', + height: '100%', + margin: 0, + padding: '0 1rem', + boxSizing: 'border-box', + font: 'inherit', + border: 0, + backgroundColor: 'transparent', + color: 'inherit', + outline: 0, + textTransform: 'uppercase', + fontWeight: 'bolder', + position: 'relative', +}) + +const VARIANTS = { + default: { + backgroundColor: 'var(--color-bg, white)', + borderColor: 'var(--color-fg, black)', + color: 'var(--color-fg, black)', + }, + primary: { + backgroundColor: 'var(--color-fg, black)', + borderColor: 'var(--color-fg, black)', + color: 'var(--color-bg, white)', + }, +} + +type Props = { + children?: ReactChild, + className?: string, + type?: 'button' | 'reset' | 'submit', + block?: boolean, + variant?: keyof typeof VARIANTS, +} + +const ActionButton: FC = ({ + children, + className, + type = 'button', + block, + variant = 'default', + ...etcProps +}) => { + return ( + + + {children} + + + ) +} + +export default ActionButton diff --git a/packages/app-web/src/components/molecules/forms/TextArea/index.test.tsx b/packages/app-web/src/components/molecules/forms/TextArea/index.test.tsx new file mode 100644 index 0000000..90a0e85 --- /dev/null +++ b/packages/app-web/src/components/molecules/forms/TextArea/index.test.tsx @@ -0,0 +1,21 @@ +import {render, cleanup} from '@testing-library/react' +import TextInput from '.' + +describe('single-line text input component', () => { + afterEach(() => { + cleanup() + }) + + it('should contain a text input element', () => { + const result = render() + const input = result.queryByRole('textbox') + expect(input).not.toBeNull() + }) + + it('should acquire a descriptive label', () => { + const label = 'foo' + const result = render() + const input = result.queryByLabelText(label) + expect(input).not.toBeNull() + }) +}) diff --git a/packages/app-web/src/components/molecules/forms/TextArea/index.tsx b/packages/app-web/src/components/molecules/forms/TextArea/index.tsx new file mode 100644 index 0000000..7d27828 --- /dev/null +++ b/packages/app-web/src/components/molecules/forms/TextArea/index.tsx @@ -0,0 +1,85 @@ +import {FC} from 'react' +import styled from 'styled-components' + +const Base = styled('div')({ + height: '3rem', + borderRadius: '0.25rem', + overflow: 'hidden', + position: 'relative', + backgroundColor: 'var(--color-bg, white)', + '::before': { + content: "''", + borderWidth: 1, + borderStyle: 'solid', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + borderRadius: 'inherit', + boxSizing: 'border-box', + }, +}) + +const ClickArea = styled('label')({ + position: 'relative', + height: '100%', +}) + +const Label = styled('span')({ + position: 'absolute', + left: -999999, +}) + +const Input = styled('textarea')({ + display: 'block', + width: '100%', + height: '100%', + margin: 0, + padding: '0 1rem', + boxSizing: 'border-box', + font: 'inherit', + border: 0, + backgroundColor: 'transparent', + color: 'inherit', + outline: 0, +}) + +type Props = { + label: string, + name: string, + className?: string, + block?: boolean, + placeholder?: string, +} + +const TextArea: FC = ({ + label, + className, + block, + ...etcProps +}) => { + return ( + + + + + + + ) +} + +export default TextArea diff --git a/packages/app-web/src/components/molecules/forms/TextInput/index.test.tsx b/packages/app-web/src/components/molecules/forms/TextInput/index.test.tsx new file mode 100644 index 0000000..90a0e85 --- /dev/null +++ b/packages/app-web/src/components/molecules/forms/TextInput/index.test.tsx @@ -0,0 +1,21 @@ +import {render, cleanup} from '@testing-library/react' +import TextInput from '.' + +describe('single-line text input component', () => { + afterEach(() => { + cleanup() + }) + + it('should contain a text input element', () => { + const result = render() + const input = result.queryByRole('textbox') + expect(input).not.toBeNull() + }) + + it('should acquire a descriptive label', () => { + const label = 'foo' + const result = render() + const input = result.queryByLabelText(label) + expect(input).not.toBeNull() + }) +}) diff --git a/packages/app-web/src/components/molecules/forms/TextInput/index.tsx b/packages/app-web/src/components/molecules/forms/TextInput/index.tsx new file mode 100644 index 0000000..e3f5e29 --- /dev/null +++ b/packages/app-web/src/components/molecules/forms/TextInput/index.tsx @@ -0,0 +1,83 @@ +import {FC} from 'react' +import styled from 'styled-components' + +const Base = styled('div')({ + height: '3rem', + borderRadius: '0.25rem', + overflow: 'hidden', + position: 'relative', + backgroundColor: 'var(--color-bg, white)', + '::before': { + content: "''", + borderWidth: 1, + borderStyle: 'solid', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + borderRadius: 'inherit', + boxSizing: 'border-box', + }, +}) + +const ClickArea = styled('label')({ + position: 'relative', + height: '100%', +}) + +const Label = styled('span')({ + position: 'absolute', + left: -999999, +}) + +const Input = styled('input')({ + display: 'block', + width: '100%', + height: '100%', + margin: 0, + padding: '0 1rem', + boxSizing: 'border-box', + font: 'inherit', + border: 0, + backgroundColor: 'transparent', + color: 'inherit', + outline: 0, +}) + +type Props = { + label: string, + name: string, + className?: string, + block?: boolean, + placeholder?: string, + type?: 'email' | 'url' | 'text' | 'tel', +} + +const TextInput: FC = ({ + label, + className, + block, + ...etcProps +}) => { + return ( + + + + + + + ) +} + +export default TextInput diff --git a/packages/app-web/src/components/molecules/navigation/Link/index.tsx b/packages/app-web/src/components/molecules/navigation/Link/index.tsx new file mode 100644 index 0000000..d2c4ab6 --- /dev/null +++ b/packages/app-web/src/components/molecules/navigation/Link/index.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import NextLink from 'next/link' +import {UrlObject} from 'url' + +type Props = { + href: UrlObject, + as?: UrlObject, + prefetch?: boolean, + replace?: boolean, + shallow?: boolean, + component?: React.ElementType, +} + +const Link: React.FC = ({ + href, + as, + prefetch, + replace, + shallow, + component: Component = 'a', + ...etcProps +}) => { + return ( + + + + ) +} + +export default Link diff --git a/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx b/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx new file mode 100644 index 0000000..d151173 --- /dev/null +++ b/packages/app-web/src/components/organisms/forms/CreateRingtone/index.tsx @@ -0,0 +1,37 @@ +import {FC, FormEventHandler} from 'react' +import styled from 'styled-components' +import TextInput from '../../../molecules/forms/TextInput' +import TextArea from '../../../molecules/forms/TextArea' +import ActionButton from '../../../molecules/forms/ActionButton' + +const Form = styled('form')({ + display: 'grid', + gap: '1rem', +}) + +type Props = { + onSubmit?: FormEventHandler +} + +const CreateRingtoneForm: FC = ({ + onSubmit, +}) => { + return ( +
+ +