Add E2E tests using Cypress

This commit is contained in:
Misode
2021-12-26 02:48:21 +01:00
parent 1e4f82129a
commit ffb2949f05
16 changed files with 2941 additions and 16 deletions

View File

@@ -6,7 +6,7 @@ module.exports = {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"tsconfigRootDir": __dirname,
"project": "./tsconfig.json"
"project": "./tsconfig-eslint.json"
},
"plugins": [
"@typescript-eslint"
@@ -14,8 +14,7 @@ module.exports = {
"ignorePatterns": [
"**/node_modules",
"**/dist",
".eslintrc.js",
"vite.config.js"
".eslintrc.js"
],
"rules": {
"@typescript-eslint/consistent-type-imports": [

3
cypress.json Normal file
View File

@@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:3000"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,64 @@
describe('Generators', () => {
beforeEach(() => {
cy.visit('/loot-table/')
cy.contains('h1', 'Loot Table Generator').should('exist')
cy.contains('label', 'Pools').should('exist')
})
it('can change an input field', () => {
cy.get('label:contains("Rolls") ~ input').clear().type('4').blur()
cy.get('[data-cy=import-area]').should('contain.value', '"rolls": 4')
})
it('can change a dropdown', () => {
cy.get('label:contains("Type") ~ select').first().select('minecraft:entity')
cy.get('[data-cy=import-area]').should('contain.value', '"type": "minecraft:entity"')
})
it('can add an element to an array', () => {
cy.get('label:contains("Pools") ~ button').click()
cy.get('[data-cy=tree]')
.find('label:contains("Rolls")').should('have.length', 2)
})
it('can remove an element from an array', () => {
cy.contains('label', 'Pools')
.get('button[aria-label="Remove"]').first().click()
cy.get('label:contains("Rolls")').should('not.exist')
})
it('can change a boolean field', () => {
cy.get('label:contains("Functions") ~ button').first().click()
cy.get('label:contains("Function") ~ select').should('have.value', 'minecraft:set_count')
cy.get('label:contains("Add") ~ button:contains("False")').click()
cy.get('[data-cy=import-area]').should('contain.value', '"add": false')
cy.get('label:contains("Add") ~ button:contains("False")').click()
cy.get('[data-cy=import-area]').should('not.contain.value', '"add": false')
cy.get('label:contains("Add") ~ button:contains("True")').click()
cy.get('[data-cy=import-area]').should('contain.value', '"add": true')
})
it('can collapse an element', () => {
cy.contains('label', 'Pools')
.get('button[aria-label^="Collapse"]').first().click()
cy.get('label:contains("Rolls")').should('not.exist')
cy.contains('label', 'Pools')
.get('button[aria-label^="Expand"]').first().click()
cy.get('[data-cy=tree]')
.find('label:contains("Rolls")').should('have.length', 1)
})
it('can move elements in an array', () => {
cy.get('label:contains("Rolls") ~ input').clear().type('4').blur()
cy.get('[data-cy=import-area]').should('contain.value', '"rolls": 4')
cy.get('label:contains("Rolls") ~ input').first().should('have.value', '4')
cy.get('label:contains("Pools") ~ button').click()
cy.get('[data-cy=tree]')
.find('label:contains("Rolls")').should('have.length', 2)
cy.get('label:contains("Rolls") ~ input').first().should('have.value', '1')
cy.get('button[aria-label="Move down"]').first().click()
cy.get('label:contains("Rolls") ~ input').first().should('have.value', '4')
cy.get('button[aria-label="Move up"]').last().click()
cy.get('label:contains("Rolls") ~ input').first().should('have.value', '1')
})
})

View File

@@ -0,0 +1,86 @@
describe('Settings and navigation', () => {
describe('Homepage', () => {
beforeEach(() => {
cy.visit('/')
cy.contains('h1', 'Data Pack Generators').should('exist')
})
it('can switch themes', () => {
cy.get('[data-cy=theme-switcher]').click()
.contains('Light').click()
cy.get('html').invoke('attr', 'data-theme').should('eq', 'light')
})
it('can switch languages', () => {
cy.get('[data-cy=language-switcher]').click()
.contains('Deutsch').click()
cy.contains('h1', 'Datenpaketgeneratoren').should('exist')
})
it('can open a generator', () => {
cy.contains('Loot Table').click()
cy.url().should('contain', '/loot-table')
cy.contains('h1', 'Loot Table Generator').should('exist')
cy.contains('label', 'Pools').should('exist')
})
})
describe('Generators', () => {
beforeEach(() => {
cy.visit('/loot-table/')
cy.contains('h1', 'Loot Table Generator').should('exist')
cy.contains('label', 'Pools').should('exist')
})
it('can switch between generators', () => {
cy.get('[data-cy=generator-switcher]').click()
.contains('Predicate').click()
cy.url().should('contain', '/predicate')
cy.contains('h1', 'Predicate Generator').should('exist')
})
it('can switch schema versions', () => {
cy.contains('Constant').should('exist')
cy.contains('Exact').should('not.exist')
cy.get('[data-cy=version-switcher]').click()
.contains('1.16').click()
cy.contains('Exact').should('exist')
cy.contains('Constant').should('not.exist')
})
it('can load a preset', () => {
cy.contains('Presets').click()
cy.contains('blocks/acacia_log').click()
cy.get('label:contains("Name") ~ input').should('have.value', 'minecraft:acacia_log')
})
it('can import JSON', () => {
cy.contains('Import').click()
cy.get('[data-cy=import-area]')
.type('{"pools":[{"rolls":2,"entries":[{"type":"minecraft:item","name":"minecraft:diamond"}]}]}', { parseSpecialCharSequences: false })
.blur()
cy.get('label:contains("Name") ~ input').should('have.value', 'minecraft:diamond')
cy.get('label:contains("Rolls") ~ input').should('have.value', '2')
})
it('can output YAML', () => {
cy.get('[data-cy=source-controls]').click()
.contains('YAML').click()
cy.get('[data-cy=import-area]').should('have.value', "pools:\n - rolls: 1\n entries:\n - type: 'minecraft:item'\n name: 'minecraft:stone'\n")
})
it('can output minified', () => {
cy.get('[data-cy=source-controls]').click()
.contains('Minified').click()
cy.get('[data-cy=import-area]').should('have.value', '{"pools":[{"rolls":1,"entries":[{"type":"minecraft:item","name":"minecraft:stone"}]}]}\n')
})
it('can go back to the homepage', () => {
cy.get('[data-cy=home-link]').click()
cy.url().should('not.contain', '/loot-table')
cy.contains('h1', 'Data Pack Generators').should('exist')
})
})
})

2
cypress/plugins/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export default (on, config) => {
}

0
cypress/support/index.ts Normal file
View File

8
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

2752
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@
"@types/seedrandom": "^2.4.28",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"cypress": "^9.2.0",
"eslint": "^7.27.0",
"preact": "^10.5.13",
"preact-router": "^3.2.1",

View File

@@ -3,17 +3,18 @@ import type { Octicon } from '.'
import { Btn } from '.'
import { useFocus } from '../hooks'
type BtnMenuProps = {
interface BtnMenuProps extends JSX.HTMLAttributes<HTMLDivElement> {
icon?: keyof typeof Octicon,
label?: string,
relative?: boolean,
tooltip?: string,
children: ComponentChildren,
}
export function BtnMenu({ icon, label, relative, tooltip, children }: BtnMenuProps) {
export function BtnMenu(props: BtnMenuProps) {
const { icon, label, relative, tooltip, children } = props
const [active, setActive] = useFocus()
return <div class={`btn-menu${relative === false ? ' no-relative' : ''}`}>
return <div class={`btn-menu${relative === false ? ' no-relative' : ''}`} {...props}>
<Btn {...{icon, label, tooltip}} onClick={setActive} />
{active && <div class="btn-group">
{children}

View File

@@ -27,9 +27,9 @@ export function Header({ lang, title, version, theme, changeTheme, language, cha
return <header>
<div class="title">
<Link class="home-link" href="/" aria-label={loc('home')}>{Icons.home}</Link>
<Link class="home-link" href="/" aria-label={loc('home')} data-cy="home-link">{Icons.home}</Link>
<h1>{title}</h1>
{gen && <BtnMenu icon="chevron_down" tooltip={loc('switch_generator')}>
{gen && <BtnMenu icon="chevron_down" tooltip={loc('switch_generator')} data-cy="generator-switcher">
{config.generators
.filter(g => g.category === gen?.category && checkVersion(version, g.minVersion))
.map(g =>
@@ -39,7 +39,7 @@ export function Header({ lang, title, version, theme, changeTheme, language, cha
</div>
<nav>
<ul>
<li>
<li data-cy="language-switcher">
<BtnMenu icon="globe" tooltip={loc('language')}>
{config.languages.map(({ code, name }) =>
<Btn label={name} active={code === language}
@@ -47,7 +47,7 @@ export function Header({ lang, title, version, theme, changeTheme, language, cha
)}
</BtnMenu>
</li>
<li>
<li data-cy="theme-switcher">
<BtnMenu icon={Themes[theme]} tooltip={loc('theme')}>
{Object.entries(Themes).map(([th, icon]) =>
<Btn icon={icon} label={loc(`theme.${th}`)} active={th === theme}

View File

@@ -25,7 +25,7 @@ const FORMATS: Record<string, {
}> = {
json: {
parse: json.parse,
stringify: (v, i) => json.stringify(v, null, i),
stringify: (v, i) => json.stringify(v, null, i) + '\n',
},
yaml: {
parse: yaml.load,
@@ -57,7 +57,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
const getOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates })
return FORMATS[format].stringify(data, INDENT[indent]) + '\n'
return FORMATS[format].stringify(data, INDENT[indent])
}, [indent, format])
useEffect(() => {
@@ -136,7 +136,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
return <>
<div class="controls">
<BtnMenu icon="gear" tooltip={loc('output_settings')}>
<BtnMenu icon="gear" tooltip={loc('output_settings')} data-cy="source-controls">
{Object.entries(INDENT).map(([key]) =>
<Btn label={loc(`indentation.${key}`)} active={indent === key}
onClick={() => changeIndent(key)}/>
@@ -147,7 +147,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
onClick={() => changeFormat(key)} />)}
</BtnMenu>
</div>
<textarea ref={source} class="source" onBlur={onImport} spellcheck={false} autocorrect="off" placeholder={loc('source_placeholder')}></textarea>
<textarea ref={source} class="source" onBlur={onImport} spellcheck={false} autocorrect="off" placeholder={loc('source_placeholder')} data-cy="import-area"></textarea>
<a ref={download} style="display: none;"></a>
</>
}

View File

@@ -25,7 +25,7 @@ export function Tree({ lang, version, model, blockStates, onError }: TreePanelPr
setState(state => state + 1)
})
return <div class="tree">
return <div class="tree" data-cy="tree">
<FullNode {...{model, lang, version, blockStates}}/>
</div>
}

View File

@@ -206,7 +206,7 @@ export function Generator({ lang, changeTitle, version, changeVersion }: Generat
</div>
{presetResults.length === 0 && <Btn label={loc('no_presets')}/>}
</BtnMenu>
<BtnMenu icon="tag" label={version}>
<BtnMenu icon="tag" label={version} data-cy="version-switcher">
{allowedVersions.reverse().map(v =>
<Btn label={v} active={v === version} onClick={() => changeVersion(v)} />
)}

8
tsconfig-eslint.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"./src",
"./cypress",
"vite.config.js"
]
}