Skip to content

Commit 1042e51

Browse files
authored
refactor: field type switching (#197)
* refactor: field type switching * chore: add test * chore: use nanoid
1 parent 46c2593 commit 1042e51

File tree

34 files changed

+2079
-368
lines changed

34 files changed

+2079
-368
lines changed

__mocks__/lodash-es.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// Mock for lodash-es module
2-
const isEqual = jest.fn((a: any, b: any) => {
3-
return true;
4-
});
2+
const isEqual = jest.fn((a: any, b: any) => true);
53

64
const debounce = jest.fn((func: Function, wait?: number) => {
75
const debouncedFn = jest.fn((...args: any[]) => {
@@ -14,5 +12,9 @@ const debounce = jest.fn((func: Function, wait?: number) => {
1412
return debouncedFn;
1513
});
1614

15+
const some = <T>(arr: T[], predicate: (v: T) => boolean) => arr.some(predicate);
16+
const every = <T>(arr: T[], predicate: (v: T) => boolean) => arr.every(predicate);
17+
const filter = <T>(arr: T[], predicate: (v: T) => boolean) => arr.filter(predicate);
18+
1719
export default isEqual;
18-
export { isEqual, debounce };
20+
export { isEqual, debounce, some, every, filter };
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Core field type conversion tests - Checkbox, Time, Checklist
3+
*
4+
* These tests exercise lazy field-type switching on web to mirror desktop behaviour.
5+
* They focus on core conversions that are reliable and deterministic.
6+
*/
7+
import { FieldType, waitForReactUpdate } from '../../support/selectors';
8+
import {
9+
generateRandomEmail,
10+
getLastFieldId,
11+
getCellsForField,
12+
getDataRowCellsForField,
13+
typeTextIntoCell,
14+
loginAndCreateGrid,
15+
addNewProperty,
16+
editLastProperty,
17+
setupFieldTypeTest,
18+
} from '../../support/field-type-test-helpers';
19+
20+
describe('Field Type - Core Conversions', () => {
21+
beforeEach(() => {
22+
setupFieldTypeTest();
23+
});
24+
25+
it('RichText ↔ Checkbox parses truthy/falsy and preserves original text', () => {
26+
const testEmail = generateRandomEmail();
27+
loginAndCreateGrid(testEmail);
28+
29+
// Add RichText property and wait for it to be ready
30+
addNewProperty(FieldType.RichText);
31+
32+
// Store field ID in alias for later use
33+
getLastFieldId().as('textFieldId');
34+
35+
// Type 'yes' into first DATA cell (eq(0) = first data row, using getDataRowCellsForField)
36+
cy.get<string>('@textFieldId').then((fieldId) => {
37+
cy.log('Typing into first cell, fieldId: ' + fieldId);
38+
return getDataRowCellsForField(fieldId).eq(0).should('exist').scrollIntoView().realClick();
39+
});
40+
cy.wait(1500);
41+
cy.get('textarea:visible', { timeout: 5000 }).should('exist').first().clear().type('yes', { delay: 30 });
42+
cy.get('body').type('{esc}');
43+
cy.wait(500);
44+
45+
// Type 'no' into second DATA cell (eq(1) = second data row)
46+
cy.get<string>('@textFieldId').then((fieldId) => {
47+
cy.log('Typing into second cell, fieldId: ' + fieldId);
48+
return getDataRowCellsForField(fieldId).eq(1).should('exist').scrollIntoView().realClick();
49+
});
50+
cy.wait(1500);
51+
cy.get('textarea:visible', { timeout: 5000 }).should('exist').first().clear().type('no', { delay: 30 });
52+
cy.get('body').type('{esc}');
53+
cy.wait(500);
54+
55+
// Switch to Checkbox
56+
editLastProperty(FieldType.Checkbox);
57+
58+
// Verify rendering shows checkbox icons - "yes" should be checked, "no" should be unchecked
59+
// Checkbox cells render as SVG icons, not text, so we check for the icon testids
60+
cy.get('[data-testid="checkbox-checked-icon"]').should('have.length.at.least', 1);
61+
cy.get('[data-testid="checkbox-unchecked-icon"]').should('have.length.at.least', 1);
62+
63+
// Switch back to RichText and ensure original raw text survives
64+
editLastProperty(FieldType.RichText);
65+
getLastFieldId().then((fieldId) => {
66+
getCellsForField(fieldId).then(($cells) => {
67+
const values: string[] = [];
68+
$cells.each((_i, el) => values.push(el.textContent || ''));
69+
expect(values.some((v) => v.toLowerCase().includes('yes'))).to.be.true;
70+
expect(values.some((v) => v.toLowerCase().includes('no'))).to.be.true;
71+
});
72+
});
73+
});
74+
75+
it('RichText ↔ Time parses HH:MM / milliseconds and round-trips', () => {
76+
const testEmail = generateRandomEmail();
77+
loginAndCreateGrid(testEmail);
78+
79+
addNewProperty(FieldType.RichText);
80+
getLastFieldId().as('timeFieldId');
81+
82+
cy.get<string>('@timeFieldId').then((fieldId) => {
83+
typeTextIntoCell(fieldId, 0, '09:30');
84+
typeTextIntoCell(fieldId, 1, '34200000');
85+
});
86+
87+
editLastProperty(FieldType.Time);
88+
89+
// Expect parsed milliseconds shown (either raw ms or formatted)
90+
getLastFieldId().then((fieldId) => {
91+
getCellsForField(fieldId).then(($cells) => {
92+
const values: string[] = [];
93+
$cells.each((_i, el) => values.push((el.textContent || '').trim()));
94+
expect(values.some((v) => v.includes('34200000') || v.includes('09:30'))).to.be.true;
95+
});
96+
});
97+
98+
// Round-trip back to RichText
99+
editLastProperty(FieldType.RichText);
100+
getLastFieldId().then((fieldId) => {
101+
getCellsForField(fieldId).then(($cells) => {
102+
const values: string[] = [];
103+
$cells.each((_i, el) => values.push((el.textContent || '').trim()));
104+
expect(values.some((v) => v.includes('09:30') || v.includes('34200000'))).to.be.true;
105+
});
106+
});
107+
});
108+
109+
it('RichText ↔ Checklist handles markdown/plain text and preserves content', () => {
110+
const testEmail = generateRandomEmail();
111+
loginAndCreateGrid(testEmail);
112+
113+
addNewProperty(FieldType.RichText);
114+
getLastFieldId().as('checklistFieldId');
115+
116+
cy.get<string>('@checklistFieldId').then((fieldId) => {
117+
typeTextIntoCell(fieldId, 0, '[x] Done\n[ ] Todo\nPlain line');
118+
});
119+
120+
editLastProperty(FieldType.Checklist);
121+
122+
// Switch back to RichText to view markdown text
123+
editLastProperty(FieldType.RichText);
124+
getLastFieldId().then((fieldId) => {
125+
getCellsForField(fieldId).then(($cells) => {
126+
const values: string[] = [];
127+
$cells.each((_i, el) => values.push((el.textContent || '').trim()));
128+
const allText = values.join('\n');
129+
expect(allText).to.match(/Done|Todo|Plain/i);
130+
});
131+
});
132+
});
133+
134+
it('Checkbox click creates checked state that survives type switch', () => {
135+
const testEmail = generateRandomEmail();
136+
loginAndCreateGrid(testEmail);
137+
138+
addNewProperty(FieldType.Checkbox);
139+
getLastFieldId().as('checkboxFieldId');
140+
141+
// Click the first checkbox to check it
142+
cy.get<string>('@checkboxFieldId').then((fieldId) => {
143+
getCellsForField(fieldId).first().click({ force: true });
144+
});
145+
waitForReactUpdate(500);
146+
147+
// Verify it's checked
148+
getLastFieldId().then((fieldId) => {
149+
getCellsForField(fieldId).first().find('[data-testid="checkbox-checked-icon"]').should('exist');
150+
});
151+
152+
// Switch to SingleSelect - should show "Yes"
153+
editLastProperty(FieldType.SingleSelect);
154+
getLastFieldId().then((fieldId) => {
155+
getCellsForField(fieldId).first().should('contain.text', 'Yes');
156+
});
157+
158+
// Switch back to Checkbox - should still be checked
159+
editLastProperty(FieldType.Checkbox);
160+
getLastFieldId().then((fieldId) => {
161+
getCellsForField(fieldId).first().find('[data-testid="checkbox-checked-icon"]').should('exist');
162+
});
163+
});
164+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* DateTime field type tests
3+
*
4+
* These tests verify DateTime field conversions and date picker interactions.
5+
*/
6+
import { FieldType, waitForReactUpdate } from '../../support/selectors';
7+
import {
8+
generateRandomEmail,
9+
getLastFieldId,
10+
getCellsForField,
11+
getDataRowCellsForField,
12+
typeTextIntoCell,
13+
loginAndCreateGrid,
14+
addNewProperty,
15+
editLastProperty,
16+
setupFieldTypeTest,
17+
} from '../../support/field-type-test-helpers';
18+
19+
describe('Field Type - DateTime', () => {
20+
beforeEach(() => {
21+
setupFieldTypeTest();
22+
});
23+
24+
it('RichText ↔ DateTime converts and preserves date data', () => {
25+
const testEmail = generateRandomEmail();
26+
loginAndCreateGrid(testEmail);
27+
28+
// Add RichText property
29+
addNewProperty(FieldType.RichText);
30+
getLastFieldId().as('dateFieldId');
31+
32+
// Enter a Unix timestamp in milliseconds (Jan 16, 2024 00:00:00 UTC)
33+
// Using a timestamp ensures consistent parsing across locales
34+
const testTimestamp = '1705363200000';
35+
36+
cy.get<string>('@dateFieldId').then((fieldId) => {
37+
typeTextIntoCell(fieldId, 0, testTimestamp);
38+
});
39+
40+
// Switch to DateTime
41+
editLastProperty(FieldType.DateTime);
42+
43+
// Verify cell renders something (DateTime cells show formatted date)
44+
// The exact format depends on locale, so we just verify the cell has content
45+
getLastFieldId().then((fieldId) => {
46+
getCellsForField(fieldId).first().then(($cell) => {
47+
const text = ($cell.text() || '').trim();
48+
// After switching to DateTime, the cell should show a formatted date
49+
cy.log(`DateTime cell content: "${text}"`);
50+
// Check that the cell has some content (date formatting varies by locale)
51+
expect(text.length).to.be.greaterThan(0);
52+
});
53+
});
54+
55+
// Switch back to RichText - the data should be preserved (as formatted date string)
56+
// Note: The lazy conversion may transform the raw timestamp into a formatted date,
57+
// which is the expected behavior for DateTime → RichText conversion
58+
editLastProperty(FieldType.RichText);
59+
getLastFieldId().then((fieldId) => {
60+
getCellsForField(fieldId).first().then(($cell) => {
61+
const text = ($cell.text() || '').trim();
62+
cy.log(`RichText cell content after round-trip: "${text}"`);
63+
// Data should be preserved (either as original timestamp or formatted date)
64+
expect(text.length).to.be.greaterThan(0);
65+
});
66+
});
67+
});
68+
69+
it('DateTime field with date picker preserves selected date through type switches', () => {
70+
const testEmail = generateRandomEmail();
71+
loginAndCreateGrid(testEmail);
72+
73+
// Add DateTime property directly
74+
addNewProperty(FieldType.DateTime);
75+
getLastFieldId().as('dateFieldId');
76+
77+
// Click on first cell to open date picker
78+
cy.get<string>('@dateFieldId').then((fieldId) => {
79+
getDataRowCellsForField(fieldId).eq(0).should('exist').scrollIntoView().click({ force: true });
80+
});
81+
waitForReactUpdate(800);
82+
83+
// Wait for the date picker popover to appear
84+
// The date picker uses a popover with calendar component
85+
cy.get('[data-testid="datetime-picker-popover"]', { timeout: 8000 }).should('be.visible');
86+
87+
// Click on today's date (which should be highlighted/selected by default)
88+
// We'll click on any available day button to set a date
89+
cy.get('[data-testid="datetime-picker-popover"]')
90+
.find('button[name="day"]')
91+
.first()
92+
.click({ force: true });
93+
waitForReactUpdate(500);
94+
95+
// Close the date picker
96+
cy.get('body').type('{esc}');
97+
waitForReactUpdate(500);
98+
99+
// Verify the cell now has a date value
100+
cy.get<string>('@dateFieldId').then((fieldId) => {
101+
getCellsForField(fieldId).first().then(($cell) => {
102+
const text = ($cell.text() || '').trim();
103+
cy.log(`DateTime cell after selection: "${text}"`);
104+
// Cell should have some date content
105+
expect(text.length).to.be.greaterThan(0);
106+
});
107+
});
108+
109+
// Switch to RichText - should show the date as text
110+
editLastProperty(FieldType.RichText);
111+
getLastFieldId().then((fieldId) => {
112+
getCellsForField(fieldId).first().then(($cell) => {
113+
const text = ($cell.text() || '').trim();
114+
cy.log(`RichText cell content: "${text}"`);
115+
// Should have some content (either formatted date or timestamp)
116+
expect(text.length).to.be.greaterThan(0);
117+
});
118+
});
119+
120+
// Switch back to DateTime - date should be preserved
121+
editLastProperty(FieldType.DateTime);
122+
getLastFieldId().then((fieldId) => {
123+
getCellsForField(fieldId).first().then(($cell) => {
124+
const text = ($cell.text() || '').trim();
125+
cy.log(`DateTime cell after round-trip: "${text}"`);
126+
// Should still have date content
127+
expect(text.length).to.be.greaterThan(0);
128+
});
129+
});
130+
});
131+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* SingleSelect and MultiSelect field type tests
3+
*
4+
* These tests verify the SingleSelect/MultiSelect ↔ RichText conversion
5+
* which is simpler to test via RichText input (avoids flaky dropdown interactions).
6+
*/
7+
import { FieldType, waitForReactUpdate } from '../../support/selectors';
8+
import {
9+
generateRandomEmail,
10+
getLastFieldId,
11+
getCellsForField,
12+
typeTextIntoCell,
13+
loginAndCreateGrid,
14+
addNewProperty,
15+
editLastProperty,
16+
setupFieldTypeTest,
17+
} from '../../support/field-type-test-helpers';
18+
19+
describe('Field Type - Select (SingleSelect/MultiSelect)', () => {
20+
beforeEach(() => {
21+
setupFieldTypeTest();
22+
});
23+
24+
it('RichText ↔ SingleSelect field type switching works without errors', () => {
25+
const testEmail = generateRandomEmail();
26+
loginAndCreateGrid(testEmail);
27+
28+
// Add RichText property and type some text
29+
addNewProperty(FieldType.RichText);
30+
getLastFieldId().as('selectFieldId');
31+
32+
cy.get<string>('@selectFieldId').then((fieldId) => {
33+
typeTextIntoCell(fieldId, 0, 'Apple');
34+
});
35+
36+
// Verify text was entered
37+
cy.get<string>('@selectFieldId').then((fieldId) => {
38+
getCellsForField(fieldId).first().should('contain.text', 'Apple');
39+
});
40+
41+
// Switch to SingleSelect - text won't match any option (expected behavior)
42+
// The lazy conversion only matches text to EXISTING options in type_option
43+
editLastProperty(FieldType.SingleSelect);
44+
waitForReactUpdate(500);
45+
46+
// Verify the field type switch happened without errors
47+
// Cell may be empty since "Apple" doesn't match any existing option
48+
cy.get<string>('@selectFieldId').then((fieldId) => {
49+
getCellsForField(fieldId).should('exist');
50+
});
51+
52+
// Switch back to RichText
53+
editLastProperty(FieldType.RichText);
54+
cy.get<string>('@selectFieldId').then((fieldId) => {
55+
// Field should still exist and be functional
56+
getCellsForField(fieldId).should('exist');
57+
});
58+
});
59+
60+
it('SingleSelect ↔ MultiSelect type switching preserves field type options', () => {
61+
const testEmail = generateRandomEmail();
62+
loginAndCreateGrid(testEmail);
63+
64+
// Add SingleSelect property - switching between Single/Multi is straightforward
65+
addNewProperty(FieldType.SingleSelect);
66+
getLastFieldId().as('selectFieldId');
67+
68+
// Switch to MultiSelect
69+
editLastProperty(FieldType.MultiSelect);
70+
waitForReactUpdate(500);
71+
72+
// Switch back to SingleSelect
73+
editLastProperty(FieldType.SingleSelect);
74+
waitForReactUpdate(500);
75+
76+
// The field should still exist and be functional (no errors during switching)
77+
// This validates the type_option conversion between Single/Multi works
78+
cy.get<string>('@selectFieldId').then((fieldId) => {
79+
getCellsForField(fieldId).should('exist');
80+
});
81+
});
82+
});

0 commit comments

Comments
 (0)