Skip to content
75 changes: 75 additions & 0 deletions examples/hooks/FieldHooks/ComputeFieldHooks/dateFormat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { TextField, Sheet, Workbook } from '@flatfile/configure'
import { SheetTester } from '../../../../src/utils/testing/SheetTester'
import { format } from 'date-fns'

//This is an example of storing the data hook outside your sheet as a helper function that can be referenced by multiple fields in your sheet.
//Here we define the function
const formatDate =
(update: string) =>
(value: string): string => {
try {
return format(new Date(value), update)
} catch (err) {
return value
}
}

Comment on lines +8 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this code actually runs. it looks like a function that returns a function. Are you sure this is what you wanted?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I see, that's waht you meant to do. Well done, you should mention this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest, this is code from @hansjhoffman that I rewrote for this example so I don't know how to best explain it 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also an example that I might remove with the launch of SmartDateField, since that would cover the basically same formatting changes this is capable of handling.

//Here we call the function in the compute hook and specify the format that should be used when reformatting the dates
const testSheet = new Sheet('Test Sheet', {
joinDate: TextField({
compute: formatDate('MM/dd/yyyy'),
}),
})

//Here we
const TestWorkbook = new Workbook({
name: 'Test Workbook',
namespace: 'Test',
sheets: { testSheet },
})

describe('Workbook tests ->', () => {
// here we use Sheet tester
const testSheet = new SheetTester(TestWorkbook, 'testSheet')
test('Change Date format with written month', async () => {
// for this inputRow
const inputRow = { joinDate: 'Nov 12, 2020' }
console.log(new Date(inputRow.joinDate))
// we expect this output row
const expectedOutputRow = { joinDate: '11/12/2020' }
const res = await testSheet.testRecord(inputRow)
//call the expectation here
expect(res).toMatchObject(expectedOutputRow)
})

//additional tests for additional cases
test('Change Date Format with dashes', async () => {
const inputRow = { joinDate: '12-31-2020' }
console.log(new Date(inputRow.joinDate))
const expectedOutputRow = { joinDate: '12/31/2020' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistic thing, I don't like defining the expected input and output so far apart.

Instead I would have the last lines as this

const inputRow = { joinDate: '12-31-2020' }
const expectedOutputRow = { joinDate: '12/31/2020' }
expect(res).toMatchObject(expectedOutputRow)

or even

expect( await testSheet.testRecord({joinDate:'12-31-2020' }).toMatchObject({joinDate:'12/31/2020'})

Generally for a bunch of test cases, you want the important code that differs between the cases to jump out at you, the boilerplate should fade away.

})

test('Change date format day-month-year', async () => {
const inputRow = { joinDate: '14 Jan 2022' }
const expectedOutputRow = { joinDate: '01/14/2022' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would have a method on sheetTester so you can test an individual field, not even caring about the name of the field. so you could just say

fieldTester = new FieldTester(TextField({
    compute: formatDate('MM/dd/yyyy'),
  }))
expect(transformField('14 Jan 2022')).toBe('01/14/2022')

})

test('Change date with period delimiters', async () => {
const inputRow = { joinDate: '12.31.2022' }
const expectedOutputRow = { joinDate: '12/31/2022' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

//This format tool does have some limitations, so knowing what some of those are so they can be handled appropriately elsewhere can be valuable
test('Date format this hook cannot handle', async () => {
const inputRow = { joinDate: '12242022' }
const expectedOutputRow = { joinDate: '12242022' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change your format function for this example, so that if it gets an unexpected input, it throws the error. Don't swallow errors and continue processing.

If any field hook throws an exception, the platform-sdk, will keep the original value of the field (double check the original value is kept, not the value to that point), and set a message on that cell of the error thrown.

I would rewrite your compute function so that it throws an error on unexpected input, then use testSheet.testMessage to verify you are getting the message (related to the error thrown) you expect for this field and input.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

})
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { TextField, Sheet, Workbook } from '@flatfile/configure'
import { SheetTester } from '../../../src/utils/testing/SheetTester'

const removeExtraSpaces = new Sheet('removeExtraSpaces', {
//here we set up a field
departmentName: TextField({
//we define a compute hook
compute: (value: any) => {
//matches two or more whitespace characters and replaces them with one space
return value.replace(/\s{2,}/g, ' ')
},
}),
})

const TestWorkbook = new Workbook({
name: 'Test Workbook',
namespace: 'Test',
sheets: { removeExtraSpaces },
})

describe('Workbook tests ->', () => {
// here we use Sheet tester
const testSheet = new SheetTester(TestWorkbook, 'removeExtraSpaces')
test('Remove Extra Leading Spaces', async () => {
// for this inputRow
const inputRow = { departmentName: ' asdf' }
// we expect this output row
const expectedOutputRow = { departmentName: ' asdf' }
const res = await testSheet.testRecord(inputRow)
//call the expectation here
expect(res).toMatchObject(expectedOutputRow)
})

//additional tests for additional cases
test('Test set of numbers', async () => {
const inputRow = { departmentName: '123456789123456' }
const expectedOutputRow = { departmentName: '123456789123456' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

test('Test trailing spaces', async () => {
const inputRow = { departmentName: 'asdf ' }
const expectedOutputRow = { departmentName: 'asdf ' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

test('Test multiple instances of extra spaces', async () => {
const inputRow = { departmentName: ' as df' }
const expectedOutputRow = { departmentName: ' as df' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

test('Test set of numbers', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change the name. this isn't a set of numbers

const inputRow = { departmentName: 'Paddy Mullen' }
const expectedOutputRow = { departmentName: 'Paddy Mullen' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})
})
49 changes: 49 additions & 0 deletions examples/hooks/FieldHooks/ComputeFieldHooks/removeSymbols.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { TextField, Sheet, Workbook } from '@flatfile/configure'
import { SheetTester } from '../../../../src/utils/testing/SheetTester'

//This hook checks for and removes special characters in a string using RegEx.
const removeSymbolsSheet = new Sheet('removeSymbolsSheet', {
//set up your field
zipCode: TextField({
//define a compute hook
compute: (value: any) => {
//add the regular expression you'll be using to search for invalid characters, then remove those characters
return value.replace(/[*;/{}\[\]"_#'^><|]/g, '')
},
}),
})

const TestWorkbook = new Workbook({
name: 'Test Workbook',
namespace: 'Test',
sheets: { removeSymbolsSheet },
})

describe('Workbook tests ->', () => {
// here we use Sheet tester
const testSheet = new SheetTester(TestWorkbook, 'removeSymbolsSheet')
test('Remove Extra Symbols', async () => {
// for this inputRow
const inputRow = { zipCode: '234**23' }
// we expect this output row
const expectedOutputRow = { zipCode: '23423' }
const res = await testSheet.testRecord(inputRow)
//call the expectation here
expect(res).toMatchObject(expectedOutputRow)
})

//additional tests for additional cases
test('Test set of numbers', async () => {
const inputRow = { zipCode: '^^55555' }
const expectedOutputRow = { zipCode: '55555' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

test('Test trailing spaces', async () => {
const inputRow = { zipCode: 'M4B [1G5]' }
const expectedOutputRow = { zipCode: 'M4B 1G5' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Message, TextField, Sheet, Workbook } from '@flatfile/configure'
import { SheetTester } from '../../../../src/utils/testing/SheetTester'

const testSheet = new Sheet('Test Sheet', {
cellPhone: TextField({
label: 'Cell Phone Number',
validate: (value: string) => {
/*the below regex matches these phone number types:
123-456-7890
(123) 456-7890
123 456 7890
123.456.7890
+91 (123) 456-7890 */
const regex = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/
if (!value.match(regex)) {
return [
new Message(
`${value} is an invalid phone number`,
'error',
'validate'
),
]
}
},
}),
})

const TestWorkbook = new Workbook({
name: 'Test Workbook',
namespace: 'Test',
sheets: { testSheet },
})

describe('Workbook tests ->', () => {
// here we use Sheet tester
const testSheet = new SheetTester(TestWorkbook, 'testSheet')
test('Reformat phone number with international code', async () => {
// for this inputRow
const inputRow = { cellPhone: '15555555555' }
// we expect this output row
const expectedOutputRow = { cellPhone: '+1 (555) 555-5555' }
const res = await testSheet.testRecord(inputRow)
//call the expectation here
expect(res).toMatchObject(expectedOutputRow)
})

//additional tests for additional cases
test('Remove letters, number too short', async () => {
const inputRow = { cellPhone: '12321 ggg' }
const expectedOutputRow = { cellPhone: 'Invalid phone number' }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paddymul this hook is waiting on the ability to test the messages that are returned, since it's a Validate hook. That's why my tests are incomplete here.

// test('Test trailing spaces', async () => {
// const inputRow = { cellPhone: 'asdf ' }
// const expectedOutputRow = { cellPhone: 'asdf ' }
// const res = await testSheet.testRecord(inputRow)
// expect(res).toMatchObject(expectedOutputRow)
// })

// test('Test multiple instances of extra spaces', async () => {
// const inputRow = { cellPhone: ' as df' }
// const expectedOutputRow = { cellPhone: ' as df' }
// const res = await testSheet.testRecord(inputRow)
// expect(res).toMatchObject(expectedOutputRow)
// })

// test('Test set of numbers', async () => {
// const inputRow = { cellPhone: 'Paddy Mullen' }
// const expectedOutputRow = { cellPhone: 'Paddy Mullen' }
// const res = await testSheet.testRecord(inputRow)
// expect(res).toMatchObject(expectedOutputRow)
// })
})
51 changes: 51 additions & 0 deletions examples/hooks/RecordHooks/conditionallyEmptyField.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NumberField, TextField, Sheet, Workbook } from '@flatfile/configure'
import { SheetTester } from '../../../src/utils/testing/SheetTester'

//this is a basic example of how to modify a field based on other fields
const testSheet = new Sheet(
'Test Sheet',
{
paymentStatus: TextField(),
amount: NumberField(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note. we are encouraging all Fields to be called with at least an empty object {} so this becomes
NumberField({})

},
{
//because this hook is comparing multiple fields and both modifying a field and adding a message, it must be run as a recordHook rather than a fieldHook
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend:
//because this hook is comparing multiple fields and both modifying a field, it must be run as a recordHook rather than a fieldHook

Adding a message at the same time shouldn't be promoted as a reason to use recordCompute... The only way you could properly set the warning message for 'payment' status is by looking at 'paymentStatus' and 'amount', that's why you want recordCompute.

recordCompute: (record, session, logger) => {
if (record.get('paymentStatus') && !record.get('amount')) {
record.set('paymentStatus', '')
record.addWarning(
'paymentStatus',
'Amount must be greater than 0 to select a Payment Status'
)
}
},
}
)

const TestWorkbook = new Workbook({
name: 'Test Workbook',
namespace: 'Test',
sheets: { testSheet },
})

describe('Workbook tests ->', () => {
// here we use Sheet tester
const testSheet = new SheetTester(TestWorkbook, 'testSheet')
test('Empty the Cell', async () => {
// for this inputRow
const inputRow = { paymentStatus: 'Paid', amount: null }
// we expect this output row
const expectedOutputRow = { paymentStatus: '', amount: null }
const res = await testSheet.testRecord(inputRow)
//call the expectation here
expect(res).toMatchObject(expectedOutputRow)
})

//additional tests for additional cases
test('test value in paymentStatus', async () => {
const inputRow = { paymentStatus: 'Paid', amount: 2500 }
const expectedOutputRow = { paymentStatus: 'Paid', amount: 2500 }
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})
})
76 changes: 76 additions & 0 deletions examples/hooks/RecordHooks/conditionallyModifyField.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Sheet, Workbook, BooleanField, TextField } from '@flatfile/configure'
import { SheetTester } from '../../../src/utils/testing/SheetTester'

//this Data Hook can be used to update the contents of a cell based on whether or not another cell passes RegEx validation.
//This method of setting fields at validations at the top of a sheet allows you to set all RegEx and messages in one easily referencable place.
const FIELDS = {
accountId: {
regex: /^.{1,20}$/,
message: 'Account Number must be 20 characters or less',
},
}

const testSheet = new Sheet(
'Test Sheet',
{
accountId: TextField(),
accountStatus: BooleanField(),
},
{
recordCompute: (record) => {
//ensuring the accountId is returned as a string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because accountID is a TextField we know it will be string or possibly null. Given our existing typing implemenation, we only know that accountID could be one of null, string, number, date, boolean. Try this code instead:

//make sure we actually have a string, then continue processing
const accountID = record.get('accountID')
if(typeof accountID === 'string') { // this type guard assures typescript that we are dealing with a string
     if (FIELDS['accountId'].regex.test(accountNum) === false) {
        record.set('accountStatus', false)
        record.addError('accountId', FIELDS.accountId.message)
      } else {
        record.set('accountStatus', true)
      }

I would recommend explicitly handling the else case, where accountID is null.

const accountNum = record.get('accountId') as string
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paddymul I needed to ensure I was returning the accountId field as a string here otherwise TypeScript was concerned that the TPrimitive type could be a number, boolean, string, or null. Is there a better way to ensure this is a string to be able to test properly?

//here we reference the fields we defined at the top, the RegEx, and the error message we want to display if the field does not pass its RegEx.
if (FIELDS['accountId'].regex.test(accountNum) === false) {
record.set('accountStatus', false)
record.addError('accountId', FIELDS.accountId.message)
} else {
record.set('accountStatus', true)
}
},
}
)

const TestWorkbook = new Workbook({
name: 'Test Workbook',
namespace: 'Test',
sheets: { testSheet },
})

describe('Workbook tests ->', () => {
// here we use Sheet tester
const testSheet = new SheetTester(TestWorkbook, 'testSheet')
test('21 Characters', async () => {
// for this inputRow
const inputRow = { accountId: '1234567890asdfghjkjhg', accountStatus: true }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are tests that acutally need testRecord because they deal with multiple fields.

// we expect this output row
const expectedOutputRow = {
accountId: '1234567890asdfghjkjhg',
accountStatus: false,
}
const res = await testSheet.testRecord(inputRow)
//call the expectation here
expect(res).toMatchObject(expectedOutputRow)
})

//additional tests for additional cases
test('19 Characters', async () => {
const inputRow = { accountId: '1234567890asdfghjkj', accountStatus: true }
const expectedOutputRow = {
accountId: '1234567890asdfghjkj',
accountStatus: true,
}
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})

test('Test Unexpected Characters', async () => {
const inputRow = { accountId: 'd sdfl &&^$% 23', accountStatus: true }
const expectedOutputRow = {
accountId: 'd sdfl &&^$% 23',
accountStatus: true,
}
const res = await testSheet.testRecord(inputRow)
expect(res).toMatchObject(expectedOutputRow)
})
})