Skip to content
Open
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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 9

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- run: pnpm install

- run: pnpm -r --filter './packages/*' build

- name: Run tests with coverage
run: pnpm -r test -- --coverage
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ dist/
# pnpm
.pnpm-debug.log*

# Coverage
coverage/
.coverage/

# Local settings
*.local.json

Expand Down
5 changes: 4 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"import": "./dist/index.js"
}
},
"files": ["dist"],
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
Expand All @@ -22,6 +24,7 @@
"@faker-js/faker": "^10.3.0"
},
"devDependencies": {
"@vitest/coverage-v8": "^3.0.0",
"tsup": "^8.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
Expand Down
308 changes: 304 additions & 4 deletions packages/core/src/fields.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { describe, it, expect } from 'vitest'
import {
text, textarea, time, select, multiselect, list, tree,
date, datetime, number, currency, file, image, toggle, checkbox,
Text, TextFieldDefinition, TimeFieldDefinition,
SelectFieldDefinition, MultiSelectFieldDefinition,
ListFieldDefinition, TreeFieldDefinition,
DateFieldDefinition, DatetimeFieldDefinition,
NumberFieldDefinition, CurrencyFieldDefinition,
FileFieldDefinition, ToggleFieldDefinition, CheckboxFieldDefinition,
} from './fields'
import { action, ActionDefinition } from './action'
import { group, GroupDefinition } from './group'
import { Scope, Position } from './types'

describe('text()', () => {
it('creates a TextFieldDefinition with component "text"', () => {
Expand Down Expand Up @@ -176,15 +182,309 @@ describe('tree()', () => {
})
})

describe('action().condition()', () => {
it('adds condition callback to config', () => {
describe('action()', () => {
it('defaults to variant "default"', () => {
expect(action().toConfig().variant).toBe('default')
})

it('condition() adds callback to config', () => {
const fn = (r: Record<string, unknown>) => r.status === 'pending'
const config = action().primary().condition(fn).toConfig()
expect(config.condition).toBe(fn)
})

it('config without condition has no condition property', () => {
const config = action().primary().toConfig()
expect(config.condition).toBeUndefined()
expect(action().primary().toConfig().condition).toBeUndefined()
})

it.each([
['destructive', 'destructive'],
['secondary', 'secondary'],
['muted', 'muted'],
['accent', 'accent'],
['success', 'success'],
['warning', 'warning'],
['info', 'info'],
] as const)('%s() sets variant', (method, expected) => {
const config = (action() as any)[method]().toConfig()
expect(config.variant).toBe(expected)
})

it('positions() sets positions array', () => {
const config = action().positions(Position.top, Position.footer).toConfig()
expect(config.positions).toEqual([Position.top, Position.footer])
})

it('start() sets align to "start"', () => {
expect(action().start().toConfig().align).toBe('start')
})

it('end() sets align to "end"', () => {
expect(action().end().toConfig().align).toBe('end')
})

it('order() sets order value', () => {
expect(action().order(5).toConfig().order).toBe(5)
})

it('hidden() sets hidden to true', () => {
expect(action().hidden().toConfig().hidden).toBe(true)
})

it('hidden(false) sets hidden to false', () => {
expect(action().hidden(false).toConfig().hidden).toBe(false)
})

it('open() sets open to true', () => {
expect(action().open().toConfig().open).toBe(true)
})

it('open(false) sets open to false', () => {
expect(action().open(false).toConfig().open).toBe(false)
})

it('scopes() restricts to specified scopes', () => {
const config = action().scopes(Scope.add, Scope.edit).toConfig()
expect(config.scopes).toEqual([Scope.add, Scope.edit])
})

it('excludeScopes() computes complement', () => {
const config = action().excludeScopes(Scope.index).toConfig()
expect(config.scopes).toEqual([Scope.add, Scope.view, Scope.edit])
})

it('supports method chaining', () => {
const config = action().primary().start().order(1).positions(Position.top).hidden().toConfig()
expect(config.variant).toBe('primary')
expect(config.align).toBe('start')
expect(config.order).toBe(1)
expect(config.hidden).toBe(true)
})
})

describe('FieldDefinition base methods', () => {
it('hidden() sets form.hidden to true', () => {
expect(text().hidden().toConfig().form.hidden).toBe(true)
})

it('hidden(false) sets form.hidden to false', () => {
expect(text().hidden(false).toConfig().form.hidden).toBe(false)
})

it('disabled() sets form.disabled to true', () => {
expect(text().disabled().toConfig().form.disabled).toBe(true)
})

it('disabled(false) sets form.disabled to false', () => {
expect(text().disabled(false).toConfig().form.disabled).toBe(false)
})

it('order() sets form.order', () => {
expect(text().order(3).toConfig().form.order).toBe(3)
})

it('default() sets defaultValue', () => {
expect(text().default('hello').toConfig().defaultValue).toBe('hello')
})

it('group() sets group key', () => {
expect(text().group('info').toConfig().group).toBe('info')
})

it('scopes() restricts field to specific scopes', () => {
const config = text().scopes(Scope.add, Scope.edit).toConfig()
expect(config.scopes).toEqual([Scope.add, Scope.edit])
})

it('excludeScopes() computes complement', () => {
const config = text().excludeScopes(Scope.index).toConfig()
expect(config.scopes).toEqual([Scope.add, Scope.view, Scope.edit])
})

it('states() sets states array', () => {
expect(text().states('draft', 'published').toConfig().states).toEqual(['draft', 'published'])
})

it('column() enables table.show', () => {
expect(text().column().toConfig().table.show).toBe(true)
})

it('column() with config merges into table', () => {
const config = text().column({ width: 200, align: 'center' }).toConfig()
expect(config.table.show).toBe(true)
expect(config.table.width).toBe(200)
expect(config.table.align).toBe('center')
})

it('filterable() sets table.filterable', () => {
expect(text().filterable().toConfig().table.filterable).toBe(true)
})

it('sortable(false) sets table.sortable to false', () => {
expect(text().sortable(false).toConfig().table.sortable).toBe(false)
})
})

describe('text().pattern()', () => {
it('adds pattern validation rule', () => {
const regex = /^\d+$/
const config = text().pattern(regex, 'numbers only').toConfig()
expect(config.validations).toEqual([
{ rule: 'pattern', params: { regex, message: 'numbers only' } },
])
})
})

describe('list().itemSchema()', () => {
it('stores schema in attrs', () => {
const fakeSchema = { domain: 'item' } as any
const config = list().itemSchema(fakeSchema).toConfig()
expect(config.attrs.itemSchema).toEqual(fakeSchema)
})
})

describe('date()', () => {
it('creates a DateFieldDefinition', () => {
const def = date()
expect(def).toBeInstanceOf(DateFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('date')
expect(config.dataType).toBe('date')
})

it('min() and max() add date validations', () => {
const config = date().min('2024-01-01').max('2024-12-31').toConfig()
expect(config.validations).toEqual([
{ rule: 'minDate', params: { value: '2024-01-01' } },
{ rule: 'maxDate', params: { value: '2024-12-31' } },
])
})
})

describe('datetime()', () => {
it('creates a DatetimeFieldDefinition', () => {
const def = datetime()
expect(def).toBeInstanceOf(DatetimeFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('datetime')
expect(config.dataType).toBe('datetime')
})

it('min() and max() add date validations', () => {
const config = datetime().min('2024-01-01T00:00').max('2024-12-31T23:59').toConfig()
expect(config.validations).toEqual([
{ rule: 'minDate', params: { value: '2024-01-01T00:00' } },
{ rule: 'maxDate', params: { value: '2024-12-31T23:59' } },
])
})
})

describe('number()', () => {
it('creates a NumberFieldDefinition', () => {
const def = number()
expect(def).toBeInstanceOf(NumberFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('number')
expect(config.dataType).toBe('number')
})

it('min() and max() add validations', () => {
const config = number().min(0).max(100).toConfig()
expect(config.validations).toEqual([
{ rule: 'min', params: { value: 0 } },
{ rule: 'max', params: { value: 100 } },
])
})

it('precision() sets attrs.precision', () => {
expect(number().precision(2).toConfig().attrs.precision).toBe(2)
})
})

describe('currency()', () => {
it('creates a CurrencyFieldDefinition', () => {
const def = currency()
expect(def).toBeInstanceOf(CurrencyFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('currency')
expect(config.dataType).toBe('number')
})

it('min() and max() add validations', () => {
const config = currency().min(0).max(9999).toConfig()
expect(config.validations).toEqual([
{ rule: 'min', params: { value: 0 } },
{ rule: 'max', params: { value: 9999 } },
])
})

it('precision() and prefix() set attrs', () => {
const config = currency().precision(2).prefix('R$').toConfig()
expect(config.attrs.precision).toBe(2)
expect(config.attrs.prefix).toBe('R$')
})
})

describe('file()', () => {
it('creates a FileFieldDefinition with component "file"', () => {
const def = file()
expect(def).toBeInstanceOf(FileFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('file')
expect(config.dataType).toBe('file')
})

it('accept() sets attrs.accept', () => {
expect(file().accept('.pdf,.doc').toConfig().attrs.accept).toBe('.pdf,.doc')
})

it('maxSize() adds validation', () => {
const config = file().maxSize(5_000_000).toConfig()
expect(config.validations).toEqual([
{ rule: 'maxSize', params: { value: 5_000_000 } },
])
})
})

describe('image()', () => {
it('creates a FileFieldDefinition with component "image"', () => {
const config = image().toConfig()
expect(config.component).toBe('image')
expect(config.dataType).toBe('file')
})
})

describe('toggle()', () => {
it('creates a ToggleFieldDefinition', () => {
const def = toggle()
expect(def).toBeInstanceOf(ToggleFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('toggle')
expect(config.dataType).toBe('boolean')
})
})

describe('checkbox()', () => {
it('creates a CheckboxFieldDefinition', () => {
const def = checkbox()
expect(def).toBeInstanceOf(CheckboxFieldDefinition)
const config = def.toConfig()
expect(config.component).toBe('checkbox')
expect(config.dataType).toBe('boolean')
})
})

describe('group()', () => {
it('returns a GroupDefinition', () => {
expect(group()).toBeInstanceOf(GroupDefinition)
})

it('toConfig() returns empty object', () => {
expect(group().toConfig()).toEqual({})
})

it('toConfig() returns a copy', () => {
const g = group()
expect(g.toConfig()).not.toBe(g.toConfig())
})
})
Loading