Fix testing for checklists (#9332)
* Fix testing for checklists * address review comments * chevron tests * improve new item test * remove rerenders * address comments * missing empty line
This commit is contained in:
parent
d26c5c9757
commit
31fc648e50
17 changed files with 1845 additions and 77 deletions
|
|
@ -315,7 +315,7 @@ describe('renameChecklist', () => {
|
|||
expect(updated!.title).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should handle empty title string', async () => {
|
||||
it('should reject empty title string', async () => {
|
||||
const runId = 'runid';
|
||||
const checklist = {
|
||||
...TestHelper.createPlaybookChecklist(runId, 0, 0),
|
||||
|
|
@ -324,16 +324,18 @@ describe('renameChecklist', () => {
|
|||
};
|
||||
await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false});
|
||||
|
||||
const originalTitle = checklist.title;
|
||||
const {data, error} = await renameChecklist(serverUrl, checklist.id, '');
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
expect(error).toBe('Title cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Verify the title was not changed
|
||||
const updated = await getPlaybookChecklistById(operator.database, checklist.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe('');
|
||||
expect(updated!.title).toBe(originalTitle);
|
||||
});
|
||||
|
||||
it('should handle whitespace-only title', async () => {
|
||||
it('should reject whitespace-only title', async () => {
|
||||
const runId = 'runid';
|
||||
const checklist = {
|
||||
...TestHelper.createPlaybookChecklist(runId, 0, 0),
|
||||
|
|
@ -342,14 +344,16 @@ describe('renameChecklist', () => {
|
|||
};
|
||||
await operator.handlePlaybookChecklist({checklists: [checklist], prepareRecordsOnly: false});
|
||||
|
||||
const originalTitle = checklist.title;
|
||||
const whitespaceTitle = ' ';
|
||||
const {data, error} = await renameChecklist(serverUrl, checklist.id, whitespaceTitle);
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
expect(error).toBe('Title cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Verify the title was not changed
|
||||
const updated = await getPlaybookChecklistById(operator.database, checklist.id);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated!.title).toBe(whitespaceTitle);
|
||||
expect(updated!.title).toBe(originalTitle);
|
||||
});
|
||||
|
||||
it('should handle very long titles', async () => {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ export async function renameChecklist(serverUrl: string, checklistId: string, ti
|
|||
return {error: `Checklist not found: ${checklistId}`};
|
||||
}
|
||||
|
||||
// Validate title is not empty or whitespace-only
|
||||
if (!title || !title.trim()) {
|
||||
return {error: 'Title cannot be empty or whitespace-only'};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
checklist.update((c) => {
|
||||
c.title = title;
|
||||
|
|
|
|||
|
|
@ -203,35 +203,39 @@ describe('renamePlaybookRun', () => {
|
|||
expect(updatedRun.name).toBe(newName);
|
||||
});
|
||||
|
||||
it('should handle empty name string', async () => {
|
||||
it('should reject empty name string', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const originalName = runs[0].name;
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, '');
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
expect(error).toBe('Name cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Verify the name was not changed
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe('');
|
||||
expect(updatedRun.name).toBe(originalName);
|
||||
});
|
||||
|
||||
it('should handle whitespace-only name', async () => {
|
||||
it('should reject whitespace-only name', async () => {
|
||||
const runs = TestHelper.createPlaybookRuns(1, 0, 0);
|
||||
await handlePlaybookRuns(serverUrl, runs, false, false);
|
||||
|
||||
const playbookRunId = runs[0].id;
|
||||
const originalName = runs[0].name;
|
||||
const whitespaceName = ' ';
|
||||
|
||||
const {data, error} = await renamePlaybookRun(serverUrl, playbookRunId, whitespaceName);
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(data).toBe(true);
|
||||
expect(error).toBe('Name cannot be empty or whitespace-only');
|
||||
expect(data).toBeUndefined();
|
||||
|
||||
// Verify the name was not changed
|
||||
const updatedRun = await database.get<PlaybookRunModel>(PLAYBOOK_TABLES.PLAYBOOK_RUN).find(playbookRunId);
|
||||
expect(updatedRun.name).toBe(whitespaceName);
|
||||
expect(updatedRun.name).toBe(originalName);
|
||||
});
|
||||
|
||||
it('should handle very long names', async () => {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ export async function renamePlaybookRun(serverUrl: string, playbookRunId: string
|
|||
return {error: `Playbook run not found: ${playbookRunId}`};
|
||||
}
|
||||
|
||||
// Validate name is not empty or whitespace-only
|
||||
if (!name || !name.trim()) {
|
||||
return {error: 'Name cannot be empty or whitespace-only'};
|
||||
}
|
||||
|
||||
await database.write(async () => {
|
||||
run.update((r) => {
|
||||
r.name = name;
|
||||
|
|
|
|||
|
|
@ -1002,19 +1002,6 @@ describe('patchPlaybookRun', () => {
|
|||
|
||||
await expect(client.patchPlaybookRun(playbookRunId, updates)).rejects.toThrow('Network error');
|
||||
});
|
||||
|
||||
test('should handle empty updates object', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const updates = {};
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}`;
|
||||
const expectedOptions = {method: 'patch', body: updates};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.patchPlaybookRun(playbookRunId, updates);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renameChecklist', () => {
|
||||
|
|
@ -1055,20 +1042,6 @@ describe('renameChecklist', () => {
|
|||
|
||||
await expect(client.renameChecklist(playbookRunId, checklistNumber, newName)).rejects.toThrow('Network error');
|
||||
});
|
||||
|
||||
test('should handle empty name', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNumber = 1;
|
||||
const newName = '';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNumber}/rename`;
|
||||
const expectedOptions = {method: 'put', body: {title: newName}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.renameChecklist(playbookRunId, checklistNumber, newName);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addChecklistItem', () => {
|
||||
|
|
@ -1086,20 +1059,6 @@ describe('addChecklistItem', () => {
|
|||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should add item with empty title', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
const title = '';
|
||||
const expectedUrl = `/plugins/playbooks/api/v0/runs/${playbookRunId}/checklists/${checklistNum}/add`;
|
||||
const expectedOptions = {method: 'post', body: {title}};
|
||||
|
||||
jest.mocked(client.doFetch).mockResolvedValue(undefined);
|
||||
|
||||
await client.addChecklistItem(playbookRunId, checklistNum, title);
|
||||
|
||||
expect(client.doFetch).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
});
|
||||
|
||||
test('should handle network errors', async () => {
|
||||
const playbookRunId = 'run123';
|
||||
const checklistNum = 1;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import {OperationType} from '@constants/database';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {PLAYBOOK_TABLES} from '@playbooks/constants/database';
|
||||
import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run';
|
||||
import {PlaybookRunModel} from '@playbooks/database/models';
|
||||
|
||||
import {transformPlaybookChecklistItemRecord, transformPlaybookChecklistRecord, transformPlaybookRunRecord, transformPlaybookRunAttributeRecord, transformPlaybookRunAttributeValueRecord} from '.';
|
||||
|
|
@ -311,6 +312,123 @@ describe('*** PLAYBOOK_RUN Prepare Records Test ***', () => {
|
|||
expect(preparedRecord.updateAt).toBe(1620000004000);
|
||||
expect(preparedRecord.lastSyncAt).toBeGreaterThan(lastSyncAt);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunRecord: should set default type to PlaybookType when playbookId is present', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'playbook_run_type_test',
|
||||
playbook_id: 'playbook_1',
|
||||
create_at: 1620000000000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.type).toBe(PLAYBOOK_RUN_TYPES.PlaybookType);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunRecord: should set default type to ChannelChecklistType when playbookId is absent', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'playbook_run_type_test_2',
|
||||
create_at: 1620000000000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.type).toBe(PLAYBOOK_RUN_TYPES.ChannelChecklistType);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunRecord: should use updateAt fallback chain correctly', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_prepare_records', setActive: true});
|
||||
|
||||
// Test: raw.update_at should be used first
|
||||
const preparedRecord1 = await transformPlaybookRunRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'playbook_run_update_at_1',
|
||||
create_at: 1620000000000,
|
||||
update_at: 1620000001000,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(preparedRecord1.updateAt).toBe(1620000001000);
|
||||
|
||||
// Test: when update_at is missing, should fallback to create_at
|
||||
const preparedRecord2 = await transformPlaybookRunRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'playbook_run_update_at_2',
|
||||
create_at: 1620000002000,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(preparedRecord2.updateAt).toBe(1620000002000);
|
||||
|
||||
// Test: UPDATE action should use record.updateAt if raw.update_at is missing
|
||||
let existingRecord: PlaybookRunModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunModel>(PLAYBOOK_RUN).create((record) => {
|
||||
record._raw.id = 'playbook_run_update_at_3';
|
||||
record.createAt = 1620000003000;
|
||||
record.updateAt = 1620000004000;
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord3 = await transformPlaybookRunRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {
|
||||
id: 'playbook_run_update_at_3',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await database?.write(async () => {
|
||||
await database?.batch(preparedRecord3);
|
||||
});
|
||||
expect(preparedRecord3.updateAt).toBe(1620000004000);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunRecord: should handle CREATE with missing raw.id', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
playbook_id: 'playbook_1',
|
||||
create_at: 1620000000000,
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.id).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('*** PLAYBOOK_CHECKLIST Prepare Records Test ***', () => {
|
||||
it('=> transformPlaybookChecklistRecord: should return a record of type PlaybookChecklist for CREATE action', async () => {
|
||||
|
|
@ -408,7 +526,6 @@ describe('*** PLAYBOOK_CHECKLIST Prepare Records Test ***', () => {
|
|||
|
||||
it('=> transformPlaybookChecklistRecord: should keep most of the data if the partial checklist is empty', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_checklist_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
// Create an existing record to simulate the UPDATE action
|
||||
let existingRecord: PlaybookChecklistModel | undefined;
|
||||
|
|
@ -451,6 +568,47 @@ describe('*** PLAYBOOK_CHECKLIST Prepare Records Test ***', () => {
|
|||
expect(preparedRecord!.updateAt).toBe(1620000004000);
|
||||
expect(preparedRecord!.lastSyncAt).toBeGreaterThan(lastSyncAt);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookChecklistRecord: should handle CREATE with missing raw.id', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_checklist_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookChecklistRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
run_id: 'playbook_run_1',
|
||||
title: 'Test Checklist',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('=> transformPlaybookChecklistRecord: should handle missing runId', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_checklist_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookChecklistRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'checklist_undefined_run_id',
|
||||
title: 'Test Checklist',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
|
||||
// When run_id is missing and record is undefined, the transformer sets runId to undefined
|
||||
// WatermelonDB converts undefined to empty string for non-nullable string fields
|
||||
expect(preparedRecord.runId).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('*** PLAYBOOK_CHECKLIST_ITEM Prepare Records Test ***', () => {
|
||||
|
|
@ -775,6 +933,48 @@ describe('*** PLAYBOOK_CHECKLIST_ITEM Prepare Records Test ***', () => {
|
|||
|
||||
expect(preparedRecordDefault!.conditionReason).toBe('');
|
||||
});
|
||||
|
||||
it('=> transformPlaybookChecklistItemRecord: should handle CREATE with missing raw.id', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_checklist_item_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookChecklistItemRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
checklist_id: 'checklist_1',
|
||||
title: 'Test Item',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('=> transformPlaybookChecklistItemRecord: should handle missing checklistId', async () => {
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_checklist_item_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookChecklistItemRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'checklist_item_undefined_checklist_id',
|
||||
title: 'Test Item',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
|
||||
// When checklist_id is missing and record is undefined, the transformer sets checklistId to undefined
|
||||
// WatermelonDB converts undefined to empty string for non-nullable string fields
|
||||
expect(preparedRecord.checklistId).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
||||
|
|
@ -934,6 +1134,62 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord.attrs).toBe('{"placeholder": "Original"}');
|
||||
expect(preparedRecord.updateAt).toBe(1620000004000);
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeRecord: should handle CREATE with missing raw.id', async () => {
|
||||
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
group_id: 'group_1',
|
||||
name: 'Test Attribute',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeRecord: should handle completely empty raw object', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_prepare_records', setActive: true});
|
||||
|
||||
let existingRecord: PlaybookRunAttributeModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeModel>(PLAYBOOK_RUN_ATTRIBUTE).create((record) => {
|
||||
record._raw.id = 'attribute_empty_raw';
|
||||
record.groupId = 'group_1';
|
||||
record.name = 'Existing Attribute';
|
||||
record.type = 'text';
|
||||
record.targetId = 'target_1';
|
||||
record.targetType = 'playbook_run';
|
||||
record.createAt = 1620000000000;
|
||||
record.updateAt = 1620000001000;
|
||||
record.deleteAt = 0;
|
||||
record.attrs = '{"key": "value"}';
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {} as any,
|
||||
},
|
||||
});
|
||||
|
||||
await database?.write(async () => {
|
||||
await database?.batch(preparedRecord);
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.name).toBe('Existing Attribute');
|
||||
});
|
||||
});
|
||||
|
||||
describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
||||
|
|
@ -1059,4 +1315,55 @@ describe('*** PLAYBOOK_RUN_ATTRIBUTE_VALUE Prepare Records Test ***', () => {
|
|||
expect(preparedRecord.runId).toBe('playbook_run_2');
|
||||
expect(preparedRecord.value).toBe('Existing Value');
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should handle CREATE with missing raw.id', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
attribute_id: 'attribute_1',
|
||||
run_id: 'playbook_run_1',
|
||||
value: 'Test Value',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('=> transformPlaybookRunAttributeValueRecord: should handle completely empty raw object', async () => {
|
||||
const database = await createTestConnection({databaseName: 'playbook_run_attribute_value_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
let existingRecord: PlaybookRunAttributeValueModel | undefined;
|
||||
await database!.write(async () => {
|
||||
existingRecord = await database!.get<PlaybookRunAttributeValueModel>(PLAYBOOK_RUN_ATTRIBUTE_VALUE).create((record) => {
|
||||
record._raw.id = 'attribute_value_empty_raw';
|
||||
record.attributeId = 'attribute_1';
|
||||
record.runId = 'playbook_run_1';
|
||||
record.value = 'Existing Value';
|
||||
});
|
||||
});
|
||||
|
||||
const preparedRecord = await transformPlaybookRunAttributeValueRecord({
|
||||
action: OperationType.UPDATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: existingRecord,
|
||||
raw: {} as any,
|
||||
},
|
||||
});
|
||||
|
||||
await database?.write(async () => {
|
||||
await database?.batch(preparedRecord);
|
||||
});
|
||||
|
||||
expect(preparedRecord).toBeTruthy();
|
||||
expect(preparedRecord.value).toBe('Existing Value');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {goToScreen} from '@screens/navigation';
|
|||
import TestHelper from '@test/test_helper';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToParticipantPlaybooks, goToPlaybookRunWithChannelSwitch, goToEditCommand, goToSelectUser, goToSelectDate, goToPostUpdate, goToSelectPlaybook, goToStartARun} from './navigation';
|
||||
import {goToPlaybookRuns, goToPlaybookRun, goToParticipantPlaybooks, goToPlaybookRunWithChannelSwitch, goToEditCommand, goToSelectUser, goToSelectDate, goToPostUpdate, goToSelectPlaybook, goToStartARun, goToRenameChecklist, goToAddChecklistItem, goToRenamePlaybookRun, goToCreateQuickChecklist} from './navigation';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
goToScreen: jest.fn(),
|
||||
|
|
@ -483,4 +483,113 @@ describe('Playbooks Navigation', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToRenameChecklist', () => {
|
||||
it('should navigate to rename checklist screen with correct parameters', async () => {
|
||||
const currentTitle = 'Checklist Title';
|
||||
const onSave = jest.fn();
|
||||
|
||||
await goToRenameChecklist(mockIntl, Preferences.THEMES.denim, 'Run 1', currentTitle, onSave);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.checklist.rename.title',
|
||||
defaultMessage: 'Rename checklist',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_RENAME_CHECKLIST,
|
||||
'Rename checklist',
|
||||
{
|
||||
currentTitle,
|
||||
onSave,
|
||||
},
|
||||
{
|
||||
topBar: {
|
||||
subtitle: {
|
||||
text: 'Run 1',
|
||||
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToAddChecklistItem', () => {
|
||||
it('should navigate to add checklist item screen with correct parameters', async () => {
|
||||
const onSave = jest.fn();
|
||||
|
||||
await goToAddChecklistItem(mockIntl, Preferences.THEMES.denim, 'Run 1', onSave);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.checklist_item.add.title',
|
||||
defaultMessage: 'New Task',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_ADD_CHECKLIST_ITEM,
|
||||
'New Task',
|
||||
{
|
||||
onSave,
|
||||
},
|
||||
{
|
||||
topBar: {
|
||||
subtitle: {
|
||||
text: 'Run 1',
|
||||
color: changeOpacity(Preferences.THEMES.denim.sidebarHeaderTextColor, 0.72),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToRenamePlaybookRun', () => {
|
||||
it('should navigate to rename playbook run screen with correct parameters', async () => {
|
||||
const currentTitle = 'Playbook Run Title';
|
||||
const onSave = jest.fn();
|
||||
|
||||
await goToRenamePlaybookRun(mockIntl, Preferences.THEMES.denim, currentTitle, onSave);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'playbooks.playbook_run.rename.title',
|
||||
defaultMessage: 'Rename playbook run',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOK_RENAME_RUN,
|
||||
'Rename playbook run',
|
||||
{
|
||||
currentTitle,
|
||||
onSave,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('goToCreateQuickChecklist', () => {
|
||||
it('should navigate to create quick checklist screen with correct parameters', () => {
|
||||
const channelId = 'channel-id-1';
|
||||
const channelName = 'channel-name-1';
|
||||
const currentUserId = 'user-id-1';
|
||||
const currentTeamId = 'team-id-1';
|
||||
const serverUrl = 'https://test.server.com';
|
||||
|
||||
goToCreateQuickChecklist(mockIntl, channelId, channelName, currentUserId, currentTeamId, serverUrl);
|
||||
|
||||
expect(mockIntl.formatMessage).toHaveBeenCalledWith({
|
||||
id: 'mobile.playbook.create_checklist',
|
||||
defaultMessage: 'Create Checklist',
|
||||
});
|
||||
expect(goToScreen).toHaveBeenCalledWith(
|
||||
Screens.PLAYBOOKS_CREATE_QUICK_CHECKLIST,
|
||||
'Create Checklist',
|
||||
{
|
||||
channelId,
|
||||
channelName,
|
||||
currentUserId,
|
||||
currentTeamId,
|
||||
serverUrl,
|
||||
},
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,353 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, fireEvent} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import {Keyboard, View} from 'react-native';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
import {getLastCall, getLastCallForButton} from '@test/mock_helpers';
|
||||
|
||||
import AddChecklistItemBottomSheet from './add_checklist_item_bottom_sheet';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
buildNavigationButton: jest.fn(),
|
||||
popTopScreen: jest.fn(),
|
||||
setButtons: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-native', () => {
|
||||
const RN = jest.requireActual('react-native');
|
||||
RN.Keyboard = {
|
||||
dismiss: jest.fn(),
|
||||
};
|
||||
return RN;
|
||||
});
|
||||
|
||||
jest.mock('@hooks/navigation_button_pressed', () => jest.fn());
|
||||
jest.mock('@hooks/android_back_handler', () => jest.fn());
|
||||
jest.mock('@managers/security_manager', () => ({
|
||||
getShieldScreenId: jest.fn((id) => `shield-${id}`),
|
||||
}));
|
||||
|
||||
describe('AddChecklistItemBottomSheet', () => {
|
||||
const componentId = 'test-component-id' as any;
|
||||
const mockOnSave = jest.fn();
|
||||
|
||||
const mockRightButton = {
|
||||
id: 'add-checklist-item',
|
||||
enabled: false,
|
||||
color: Preferences.THEMES.denim.sidebarHeaderTextColor,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(buildNavigationButton).mockReturnValue(mockRightButton as any);
|
||||
});
|
||||
|
||||
function getBaseProps() {
|
||||
return {
|
||||
componentId,
|
||||
onSave: mockOnSave,
|
||||
};
|
||||
}
|
||||
|
||||
it('should render correctly with default props', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.props.value).toBe('');
|
||||
expect(input.props.autoFocus).toBe(true);
|
||||
|
||||
// Check label is rendered
|
||||
const label = getByText('Task name');
|
||||
expect(label).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set up navigation buttons on mount', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(buildNavigationButton).toHaveBeenCalledWith(
|
||||
'add-checklist-item',
|
||||
'playbooks.checklist_item.add.save',
|
||||
undefined,
|
||||
'Add',
|
||||
);
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [mockRightButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should register navigation button press handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(useNavButtonPressed).toHaveBeenCalledWith(
|
||||
'add-checklist-item',
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
[expect.any(Function)],
|
||||
);
|
||||
});
|
||||
|
||||
it('should register Android back handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
expect(useAndroidHardwareBackHandler).toHaveBeenCalledWith(
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update title when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
const newTitle = 'New Task Title';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
expect(input.props.value).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should enable save button when title has content', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
|
||||
// Initially disabled (empty title)
|
||||
expect(mockRightButton.enabled).toBe(false);
|
||||
|
||||
// Update with non-empty title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Task Title');
|
||||
});
|
||||
|
||||
// Button should be enabled now
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is empty', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
|
||||
// Set title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Task Title');
|
||||
});
|
||||
|
||||
// Button should be enabled now, covered by previous test, but needed to check otherwise there could be a race condition
|
||||
const checkButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [checkButton],
|
||||
});
|
||||
|
||||
// Clear title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, '');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is only whitespace', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
|
||||
// Set title to whitespace only
|
||||
act(() => {
|
||||
fireEvent.changeText(input, ' ');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onSave and close when save button is pressed with valid title', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
const taskTitle = 'New Task';
|
||||
|
||||
// Set title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, taskTitle);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'add-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith(taskTitle);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should trim title when saving', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
const taskTitle = ' Task with spaces ';
|
||||
|
||||
// Set title with spaces
|
||||
act(() => {
|
||||
fireEvent.changeText(input, taskTitle);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'add-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('Task with spaces');
|
||||
expect(mockOnSave).not.toHaveBeenCalledWith(taskTitle);
|
||||
});
|
||||
|
||||
it('should not call onSave when save button is pressed with empty title', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
// Trigger save button press without setting title
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'add-checklist-item');
|
||||
expect(lastCall).toBeDefined();
|
||||
const saveCallback = lastCall[2];
|
||||
expect(saveCallback).toBeDefined();
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
expect(popTopScreen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call onSave when save button is pressed with whitespace-only title', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
|
||||
// Set title to whitespace only
|
||||
act(() => {
|
||||
fireEvent.changeText(input, ' ');
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const lastCall = getLastCallForButton(jest.mocked(useNavButtonPressed), 'add-checklist-item');
|
||||
const saveCallback = lastCall[2];
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
expect(popTopScreen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close when Android back button is pressed', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
// Trigger back handler
|
||||
const lastCall = getLastCall(jest.mocked(useAndroidHardwareBackHandler));
|
||||
const backCallback = lastCall[1];
|
||||
act(() => {
|
||||
backCallback();
|
||||
});
|
||||
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should apply shield screen ID to container', () => {
|
||||
const props = getBaseProps();
|
||||
const renderResult = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
const getAllByType = renderResult.UNSAFE_getAllByType;
|
||||
|
||||
const views = getAllByType(View);
|
||||
const container = views.find((view: any) => view.props.nativeID === `shield-${componentId}`);
|
||||
expect(container).toBeTruthy();
|
||||
expect(container?.props.nativeID).toBe(`shield-${componentId}`);
|
||||
expect(SecurityManager.getShieldScreenId).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should update navigation button when canSave changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist_item.add.input');
|
||||
|
||||
// Initially disabled
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: false}],
|
||||
});
|
||||
|
||||
// Enable by adding text
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Task');
|
||||
});
|
||||
|
||||
// Should update with enabled button
|
||||
expect(setButtons).toHaveBeenLastCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: true}],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use correct theme color for button', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<AddChecklistItemBottomSheet {...props}/>);
|
||||
|
||||
// Verify button is created with theme color
|
||||
const buttonCalls = jest.mocked(setButtons).mock.calls;
|
||||
expect(buttonCalls.length).toBeGreaterThan(0);
|
||||
const lastCall = buttonCalls[buttonCalls.length - 1];
|
||||
const rightButtons = lastCall[1]?.rightButtons;
|
||||
expect(rightButtons?.[0]?.color).toBe(Preferences.THEMES.denim.sidebarHeaderTextColor);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -5,14 +5,32 @@ import {act, fireEvent, waitFor, within} from '@testing-library/react-native';
|
|||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {addChecklistItem} from '@playbooks/actions/remote/checklist';
|
||||
import ProgressBar from '@playbooks/components/progress_bar';
|
||||
import {goToAddChecklistItem} from '@playbooks/screens/navigation';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
import {logError} from '@utils/log';
|
||||
import {showPlaybookErrorSnackbar} from '@utils/snack_bar';
|
||||
|
||||
import Checklist from './checklist';
|
||||
import ChecklistItem from './checklist_item';
|
||||
|
||||
const serverUrl = 'test-server-url';
|
||||
|
||||
jest.mock('@context/server');
|
||||
jest.mocked(useServerUrl).mockReturnValue(serverUrl);
|
||||
|
||||
jest.mock('@components/compass_icon', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
const CompassIcon = require('@components/compass_icon').default;
|
||||
jest.mocked(CompassIcon).mockImplementation(
|
||||
(props: any) => React.createElement('CompassIcon', {testID: 'compass-icon', ...props}) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
);
|
||||
|
||||
jest.mock('./checklist_item');
|
||||
jest.mocked(ChecklistItem).mockImplementation(
|
||||
(props) => React.createElement('ChecklistItem', {testID: 'checklist-item', ...props}),
|
||||
|
|
@ -36,6 +54,15 @@ jest.mock('@playbooks/screens/navigation', () => ({
|
|||
goToAddChecklistItem: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@playbooks/actions/remote/checklist', () => ({
|
||||
renameChecklist: jest.fn(),
|
||||
addChecklistItem: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@utils/snack_bar', () => ({
|
||||
showPlaybookErrorSnackbar: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Checklist', () => {
|
||||
const mockChecklist = TestHelper.fakePlaybookChecklistModel({
|
||||
id: 'checklist-1',
|
||||
|
|
@ -290,4 +317,116 @@ describe('Checklist', () => {
|
|||
|
||||
expect(goToAddChecklistItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders edit icon in header', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntl(<Checklist {...props}/>);
|
||||
|
||||
const editButton = getByTestId('edit-checklist-button');
|
||||
expect(editButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('toggles expanded state which affects chevron icon', async () => {
|
||||
const props = getBaseProps();
|
||||
const {getByText, getByTestId, getAllByTestId} = renderWithIntl(<Checklist {...props}/>);
|
||||
|
||||
const header = getByText('Test Checklist');
|
||||
|
||||
// Initially expanded - chevron should be down
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('checklist-items-container')).toHaveAnimatedStyle({paddingVertical: 16});
|
||||
});
|
||||
const chevronIcons = getAllByTestId('compass-icon');
|
||||
const chevron = chevronIcons.find((icon) => icon.props.name === 'chevron-down' || icon.props.name === 'chevron-right');
|
||||
expect(chevron).toBeTruthy();
|
||||
expect(chevron?.props.name).toBe('chevron-down');
|
||||
|
||||
// Toggle to collapsed - chevron should change to right
|
||||
act(() => {
|
||||
fireEvent.press(header);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('checklist-items-container')).toHaveAnimatedStyle({paddingVertical: 0});
|
||||
});
|
||||
const chevronIconsCollapsed = getAllByTestId('compass-icon');
|
||||
const chevronCollapsed = chevronIconsCollapsed.find((icon) => icon.props.name === 'chevron-down' || icon.props.name === 'chevron-right');
|
||||
expect(chevronCollapsed).toBeTruthy();
|
||||
expect(chevronCollapsed?.props.name).toBe('chevron-right');
|
||||
|
||||
// Toggle back to expanded - chevron should change to down
|
||||
act(() => {
|
||||
fireEvent.press(header);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('checklist-items-container')).toHaveAnimatedStyle({paddingVertical: 16});
|
||||
});
|
||||
const chevronIconsExpanded = getAllByTestId('compass-icon');
|
||||
const chevronExpanded = chevronIconsExpanded.find((icon) => icon.props.name === 'chevron-down' || icon.props.name === 'chevron-right');
|
||||
expect(chevronExpanded).toBeTruthy();
|
||||
expect(chevronExpanded?.props.name).toBe('chevron-down');
|
||||
});
|
||||
|
||||
it('handles add item error correctly', async () => {
|
||||
const mockError = {message: 'Add item failed'};
|
||||
jest.mocked(addChecklistItem).mockResolvedValueOnce({error: mockError} as {error: Error});
|
||||
jest.mocked(goToAddChecklistItem).mockClear();
|
||||
|
||||
const props = getBaseProps();
|
||||
props.isFinished = false;
|
||||
props.isParticipant = true;
|
||||
|
||||
const {getByTestId} = renderWithIntl(<Checklist {...props}/>);
|
||||
|
||||
const addButton = getByTestId('add-checklist-item-button');
|
||||
act(() => {
|
||||
fireEvent.press(addButton);
|
||||
});
|
||||
|
||||
const onAdd = jest.mocked(goToAddChecklistItem).mock.calls[0][3];
|
||||
await act(async () => {
|
||||
await onAdd('New Item');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addChecklistItem).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
props.playbookRunId,
|
||||
props.checklistNumber,
|
||||
'New Item',
|
||||
);
|
||||
expect(showPlaybookErrorSnackbar).toHaveBeenCalled();
|
||||
expect(logError).toHaveBeenCalledWith('error on addChecklistItem', expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
it('handles successful add item without errors', async () => {
|
||||
jest.mocked(addChecklistItem).mockResolvedValueOnce({data: true} as {data: boolean});
|
||||
jest.mocked(goToAddChecklistItem).mockImplementation(async (intl, theme, playbookRunName, onAdd) => {
|
||||
onAdd('New Item');
|
||||
});
|
||||
|
||||
const props = getBaseProps();
|
||||
props.isFinished = false;
|
||||
props.isParticipant = true;
|
||||
|
||||
const {getByTestId} = renderWithIntl(<Checklist {...props}/>);
|
||||
|
||||
const addButton = getByTestId('add-checklist-item-button');
|
||||
act(() => {
|
||||
fireEvent.press(addButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addChecklistItem).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
props.playbookRunId,
|
||||
props.checklistNumber,
|
||||
'New Item',
|
||||
);
|
||||
expect(showPlaybookErrorSnackbar).not.toHaveBeenCalled();
|
||||
expect(logError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -214,7 +214,10 @@ const Checklist = ({
|
|||
</View>
|
||||
<View style={styles.progressAndEditContainer}>
|
||||
<Text style={styles.progressText}>{`${completed} / ${totalNumber} done`}</Text>
|
||||
<TouchableOpacity onPress={handleEditPress}>
|
||||
<TouchableOpacity
|
||||
onPress={handleEditPress}
|
||||
testID='edit-checklist-button'
|
||||
>
|
||||
<CompassIcon
|
||||
name='pencil-outline'
|
||||
style={styles.editIcon}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,325 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, fireEvent} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
|
||||
import RenameChecklistBottomSheet from './rename_checklist_bottom_sheet';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
buildNavigationButton: jest.fn(),
|
||||
popTopScreen: jest.fn(),
|
||||
setButtons: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@hooks/navigation_button_pressed', () => jest.fn());
|
||||
jest.mock('@hooks/android_back_handler', () => jest.fn());
|
||||
jest.mock('@managers/security_manager', () => ({
|
||||
getShieldScreenId: jest.fn((id) => `shield-${id}`),
|
||||
}));
|
||||
|
||||
describe('RenameChecklistBottomSheet', () => {
|
||||
const componentId = 'test-component-id' as any;
|
||||
const currentTitle = 'Original Checklist';
|
||||
const mockOnSave = jest.fn();
|
||||
|
||||
const mockRightButton = {
|
||||
id: 'save-checklist-name',
|
||||
enabled: false,
|
||||
color: Preferences.THEMES.denim.sidebarHeaderTextColor,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(buildNavigationButton).mockReturnValue(mockRightButton as any);
|
||||
jest.mocked(useNavButtonPressed).mockImplementation((buttonId, compId, callback) => {
|
||||
// Simulate button press when buttonId matches
|
||||
if (buttonId === 'save-checklist-name' && compId === componentId) {
|
||||
// Store callback for manual triggering in tests
|
||||
(useNavButtonPressed as any).lastCallback = callback;
|
||||
}
|
||||
});
|
||||
jest.mocked(useAndroidHardwareBackHandler).mockImplementation((compId, callback) => {
|
||||
// Store callback for manual triggering in tests
|
||||
(useAndroidHardwareBackHandler as any).lastCallback = callback;
|
||||
});
|
||||
});
|
||||
|
||||
function getBaseProps() {
|
||||
return {
|
||||
componentId,
|
||||
currentTitle,
|
||||
onSave: mockOnSave,
|
||||
};
|
||||
}
|
||||
|
||||
it('should render correctly with currentTitle', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.props.value).toBe(currentTitle);
|
||||
expect(input.props.autoFocus).toBe(true);
|
||||
|
||||
// Check label is rendered
|
||||
const label = getByText('Section name');
|
||||
expect(label).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set up navigation buttons on mount', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
expect(buildNavigationButton).toHaveBeenCalledWith(
|
||||
'save-checklist-name',
|
||||
'playbooks.checklist.rename.button',
|
||||
undefined,
|
||||
'Save',
|
||||
);
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [mockRightButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should register navigation button press handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
expect(useNavButtonPressed).toHaveBeenCalledWith(
|
||||
'save-checklist-name',
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
[expect.any(Function)],
|
||||
);
|
||||
});
|
||||
|
||||
it('should register Android back handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
expect(useAndroidHardwareBackHandler).toHaveBeenCalledWith(
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update title when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
const newTitle = 'New Checklist Title';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
expect(input.props.value).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should enable save button when title has content and is different from currentTitle', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
|
||||
// Initially disabled (same as currentTitle)
|
||||
expect(mockRightButton.enabled).toBe(false);
|
||||
|
||||
// Update with different title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'New Checklist Title');
|
||||
});
|
||||
|
||||
// Button should be enabled now
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is same as currentTitle', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
|
||||
// Change title to something different
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Button should be enabled
|
||||
const enabledButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [enabledButton],
|
||||
});
|
||||
|
||||
// Change back to original title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, currentTitle);
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const disabledButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [disabledButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is empty', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
|
||||
// Set title to something different
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Button should be enabled to ensure there are no race conditions passing as good
|
||||
const checkButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [checkButton],
|
||||
});
|
||||
|
||||
// Clear title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, '');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is only whitespace', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
|
||||
// Set title to whitespace only
|
||||
act(() => {
|
||||
fireEvent.changeText(input, ' ');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onSave and close when save button is pressed with valid title', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
const newTitle = 'New Checklist Name';
|
||||
|
||||
// Set title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith(newTitle);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should trim title when saving', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
const titleWithSpaces = ' New Checklist Name ';
|
||||
|
||||
// Set title with spaces
|
||||
act(() => {
|
||||
fireEvent.changeText(input, titleWithSpaces);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('New Checklist Name');
|
||||
expect(mockOnSave).not.toHaveBeenCalledWith(titleWithSpaces);
|
||||
});
|
||||
|
||||
it('should close when Android back button is pressed', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
// Trigger back handler
|
||||
const backCallback = (useAndroidHardwareBackHandler as any).lastCallback;
|
||||
act(() => {
|
||||
backCallback();
|
||||
});
|
||||
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update navigation button when canSave changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenameChecklistBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.checklist.rename.input');
|
||||
|
||||
// Initially disabled (same as currentTitle)
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: false}],
|
||||
});
|
||||
|
||||
// Enable by changing to different text
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Should update with enabled button
|
||||
expect(setButtons).toHaveBeenLastCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: true}],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ import {useServerUrl} from '@context/server';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {finishRun, setOwner} from '@playbooks/actions/remote/runs';
|
||||
import {PLAYBOOK_RUN_TYPES} from '@playbooks/constants/playbook_run';
|
||||
import {openUserProfileModal, popTopScreen} from '@screens/navigation';
|
||||
import {fireEvent, renderWithEverything, waitFor} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
|
@ -412,4 +413,204 @@ describe('PlaybookRun', () => {
|
|||
|
||||
expect(popTopScreen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render StatusUpdateIndicator for ChannelChecklistType', () => {
|
||||
const props = getBaseProps();
|
||||
(props.playbookRun as PlaybookRunModel).type = PLAYBOOK_RUN_TYPES.ChannelChecklistType;
|
||||
const {queryByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
expect(queryByTestId('status-update-indicator')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders StatusUpdateIndicator for PlaybookType', () => {
|
||||
const props = getBaseProps();
|
||||
(props.playbookRun as PlaybookRunModel).type = PLAYBOOK_RUN_TYPES.PlaybookType;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
expect(getByTestId('status-update-indicator')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles openOwnerProfile when owner is undefined', () => {
|
||||
const props = getBaseProps();
|
||||
props.owner = undefined;
|
||||
jest.mocked(openUserProfileModal).mockClear();
|
||||
const {queryByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// Should not crash, but owner chip won't be rendered
|
||||
expect(queryByTestId('user-chip')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles openChangeOwnerModal when owner is undefined', () => {
|
||||
const props = getBaseProps();
|
||||
props.owner = undefined;
|
||||
jest.mocked(goToSelectUser).mockClear();
|
||||
const {queryByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// Should not crash, but owner chip won't be rendered
|
||||
expect(queryByTestId('user-chip')).toBeNull();
|
||||
expect(goToSelectUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles handleSelectOwner when playbookRun is undefined', () => {
|
||||
// This test verifies the early return in handleSelectOwner
|
||||
// Since the component shows ErrorState when playbookRun is undefined,
|
||||
// we can't test the callback directly, but we verify the component handles it gracefully
|
||||
const props = getBaseProps();
|
||||
props.playbookRun = undefined;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// Component should render ErrorState when playbookRun is undefined
|
||||
expect(getByTestId('error-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles handleFinishRun when playbookRun is undefined', () => {
|
||||
const props = getBaseProps();
|
||||
props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId}));
|
||||
props.playbookRun = undefined;
|
||||
jest.mocked(Alert.alert).mockClear();
|
||||
renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// Should not show alert when playbookRun is undefined
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles channelId from model type', () => {
|
||||
const props = getBaseProps();
|
||||
(props.playbookRun as PlaybookRunModel).channelId = 'channel-id-123';
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const checklistList = getByTestId('checklist-list');
|
||||
expect(checklistList.props.channelId).toBe('channel-id-123');
|
||||
});
|
||||
|
||||
it('handles channel_id from API type', () => {
|
||||
const props = getBaseProps();
|
||||
const apiRun = TestHelper.fakePlaybookRun({
|
||||
id: 'run-1',
|
||||
name: 'Test Playbook Run',
|
||||
summary: 'Test summary',
|
||||
channel_id: 'api-channel-id-456',
|
||||
});
|
||||
props.playbookRun = apiRun;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const checklistList = getByTestId('checklist-list');
|
||||
expect(checklistList.props.channelId).toBe('api-channel-id-456');
|
||||
});
|
||||
|
||||
it('defaults channelId to empty string when missing', () => {
|
||||
const props = getBaseProps();
|
||||
const apiRun = TestHelper.fakePlaybookRun({
|
||||
id: 'run-1',
|
||||
name: 'Test Playbook Run',
|
||||
summary: 'Test summary',
|
||||
});
|
||||
const runWithoutChannelId = {...apiRun} as Record<string, unknown>;
|
||||
delete runWithoutChannelId.channel_id;
|
||||
props.playbookRun = runWithoutChannelId as unknown as PlaybookRunModel;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const checklistList = getByTestId('checklist-list');
|
||||
expect(checklistList.props.channelId).toBe('');
|
||||
});
|
||||
|
||||
it('handles lastSyncAt from model type', () => {
|
||||
const props = getBaseProps();
|
||||
(props.playbookRun as PlaybookRunModel).lastSyncAt = 99999;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const outOfDateHeader = getByTestId('out-of-date-header');
|
||||
expect(outOfDateHeader.props.lastSyncAt).toBe(99999);
|
||||
});
|
||||
|
||||
it('defaults lastSyncAt to 0 when missing', () => {
|
||||
const props = getBaseProps();
|
||||
const apiRun = TestHelper.fakePlaybookRun({
|
||||
id: 'run-1',
|
||||
name: 'Test Playbook Run',
|
||||
summary: 'Test summary',
|
||||
});
|
||||
props.playbookRun = apiRun;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const outOfDateHeader = getByTestId('out-of-date-header');
|
||||
expect(outOfDateHeader.props.lastSyncAt).toBe(0);
|
||||
});
|
||||
|
||||
it('sets isParticipant to true when owner is current user', () => {
|
||||
const props = getBaseProps();
|
||||
props.owner = TestHelper.fakeUserModel({id: props.currentUserId});
|
||||
props.participants = [];
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const checklistList = getByTestId('checklist-list');
|
||||
expect(checklistList.props.isParticipant).toBe(true);
|
||||
});
|
||||
|
||||
it('sets readOnly to true when finished even if user is participant', () => {
|
||||
const props = getBaseProps();
|
||||
props.participants.push(TestHelper.fakeUserModel({id: props.currentUserId}));
|
||||
(props.playbookRun as PlaybookRunModel).currentStatus = 'Finished';
|
||||
const {queryByText} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// Finish button should not be rendered when readOnly is true
|
||||
expect(queryByText('Finish')).toBeNull();
|
||||
});
|
||||
|
||||
it('defaults playbookRunType to PlaybookType when type is undefined but playbookId exists', () => {
|
||||
const props = getBaseProps();
|
||||
const run = props.playbookRun as PlaybookRunModel;
|
||||
delete (run as Partial<PlaybookRunModel>).type;
|
||||
run.playbookId = 'playbook-id-123';
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// StatusUpdateIndicator should be rendered for PlaybookType
|
||||
expect(getByTestId('status-update-indicator')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('defaults playbookRunType to ChannelChecklistType when type is undefined and playbookId is empty', () => {
|
||||
const props = getBaseProps();
|
||||
const run = props.playbookRun as PlaybookRunModel;
|
||||
delete (run as Partial<PlaybookRunModel>).type;
|
||||
run.playbookId = '';
|
||||
const {queryByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// StatusUpdateIndicator should not be rendered for ChannelChecklistType
|
||||
expect(queryByTestId('status-update-indicator')).toBeNull();
|
||||
});
|
||||
|
||||
it('handles playbookRunType with playbook_id from API type', () => {
|
||||
const props = getBaseProps();
|
||||
const apiRun = TestHelper.fakePlaybookRun({
|
||||
id: 'run-1',
|
||||
name: 'Test Playbook Run',
|
||||
summary: 'Test summary',
|
||||
playbook_id: 'api-playbook-id-789',
|
||||
});
|
||||
const runWithoutType = {...apiRun} as Record<string, unknown>;
|
||||
delete runWithoutType.type;
|
||||
props.playbookRun = runWithoutType as unknown as PlaybookRunModel;
|
||||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// StatusUpdateIndicator should be rendered for PlaybookType
|
||||
expect(getByTestId('status-update-indicator')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles playbookRunType with empty playbook_id from API type', () => {
|
||||
const props = getBaseProps();
|
||||
const apiRun = TestHelper.fakePlaybookRun({
|
||||
id: 'run-1',
|
||||
name: 'Test Playbook Run',
|
||||
summary: 'Test summary',
|
||||
playbook_id: '',
|
||||
});
|
||||
const runWithoutType = {...apiRun} as Record<string, unknown>;
|
||||
delete runWithoutType.type;
|
||||
props.playbookRun = runWithoutType as unknown as PlaybookRunModel;
|
||||
const {queryByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
// StatusUpdateIndicator should not be rendered for ChannelChecklistType
|
||||
expect(queryByTestId('status-update-indicator')).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -202,7 +202,16 @@ export default function PlaybookRun({
|
|||
const isFinished = isRunFinished(playbookRun);
|
||||
const readOnly = isFinished || !isParticipant;
|
||||
|
||||
const playbookRunType = useMemo(() => playbookRun?.type || 'playbook', [playbookRun]);
|
||||
const playbookRunType = useMemo(() => {
|
||||
if (!playbookRun) {
|
||||
return PLAYBOOK_RUN_TYPES.PlaybookType;
|
||||
}
|
||||
if (playbookRun.type) {
|
||||
return playbookRun.type;
|
||||
}
|
||||
const playbookId = 'playbookId' in playbookRun ? playbookRun.playbookId : (playbookRun.playbook_id || '');
|
||||
return playbookId ? PLAYBOOK_RUN_TYPES.PlaybookType : PLAYBOOK_RUN_TYPES.ChannelChecklistType;
|
||||
}, [playbookRun]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, fireEvent} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {buildNavigationButton, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
|
||||
import RenamePlaybookRunBottomSheet from './rename_playbook_run_bottom_sheet';
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
buildNavigationButton: jest.fn(),
|
||||
popTopScreen: jest.fn(),
|
||||
setButtons: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-native', () => {
|
||||
const RN = jest.requireActual('react-native');
|
||||
RN.Keyboard = {
|
||||
dismiss: jest.fn(),
|
||||
};
|
||||
return RN;
|
||||
});
|
||||
|
||||
jest.mock('@hooks/navigation_button_pressed', () => jest.fn());
|
||||
jest.mock('@hooks/android_back_handler', () => jest.fn());
|
||||
jest.mock('@managers/security_manager', () => ({
|
||||
getShieldScreenId: jest.fn((id) => `shield-${id}`),
|
||||
}));
|
||||
|
||||
describe('RenamePlaybookRunBottomSheet', () => {
|
||||
const componentId = 'test-component-id' as any;
|
||||
const currentTitle = 'Original Playbook Run';
|
||||
const mockOnSave = jest.fn();
|
||||
|
||||
const mockRightButton = {
|
||||
id: 'save-playbook-run-name',
|
||||
enabled: false,
|
||||
color: Preferences.THEMES.denim.sidebarHeaderTextColor,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(buildNavigationButton).mockReturnValue(mockRightButton as any);
|
||||
jest.mocked(useNavButtonPressed).mockImplementation((buttonId, compId, callback) => {
|
||||
// Simulate button press when buttonId matches
|
||||
if (buttonId === 'save-playbook-run-name' && compId === componentId) {
|
||||
// Store callback for manual triggering in tests
|
||||
(useNavButtonPressed as any).lastCallback = callback;
|
||||
}
|
||||
});
|
||||
jest.mocked(useAndroidHardwareBackHandler).mockImplementation((compId, callback) => {
|
||||
// Store callback for manual triggering in tests
|
||||
(useAndroidHardwareBackHandler as any).lastCallback = callback;
|
||||
});
|
||||
});
|
||||
|
||||
function getBaseProps() {
|
||||
return {
|
||||
componentId,
|
||||
currentTitle,
|
||||
onSave: mockOnSave,
|
||||
};
|
||||
}
|
||||
|
||||
it('should render correctly with currentTitle', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId, getByText} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
expect(input).toBeTruthy();
|
||||
expect(input.props.value).toBe(currentTitle);
|
||||
expect(input.props.autoFocus).toBe(true);
|
||||
|
||||
// Check label is rendered
|
||||
const label = getByText('Checklist name');
|
||||
expect(label).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set up navigation buttons on mount', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
expect(buildNavigationButton).toHaveBeenCalledWith(
|
||||
'save-playbook-run-name',
|
||||
'playbooks.playbook_run.rename.button',
|
||||
undefined,
|
||||
'Save',
|
||||
);
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [mockRightButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should register navigation button press handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
expect(useNavButtonPressed).toHaveBeenCalledWith(
|
||||
'save-playbook-run-name',
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
[expect.any(Function)],
|
||||
);
|
||||
});
|
||||
|
||||
it('should register Android back handler', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
expect(useAndroidHardwareBackHandler).toHaveBeenCalledWith(
|
||||
componentId,
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update title when text input changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
const newTitle = 'New Playbook Run Title';
|
||||
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
expect(input.props.value).toBe(newTitle);
|
||||
});
|
||||
|
||||
it('should enable save button when title has content and is different from currentTitle', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
|
||||
// Initially disabled (same as currentTitle)
|
||||
expect(mockRightButton.enabled).toBe(false);
|
||||
|
||||
// Update with different title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'New Playbook Run Title');
|
||||
});
|
||||
|
||||
// Button should be enabled now
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is same as currentTitle', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
|
||||
// Change title to something different
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Button should be enabled
|
||||
const enabledButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [enabledButton],
|
||||
});
|
||||
|
||||
// Change back to original title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, currentTitle);
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const disabledButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [disabledButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is empty', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
|
||||
// Set title to something different
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Button should be enabled to ensure there are no race conditions passing as good
|
||||
const checkButton = {
|
||||
...mockRightButton,
|
||||
enabled: true,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [checkButton],
|
||||
});
|
||||
|
||||
// Clear title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, '');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable save button when title is only whitespace', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
|
||||
// Set title to whitespace only
|
||||
act(() => {
|
||||
fireEvent.changeText(input, ' ');
|
||||
});
|
||||
|
||||
// Button should be disabled
|
||||
const updatedButton = {
|
||||
...mockRightButton,
|
||||
enabled: false,
|
||||
};
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [updatedButton],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onSave and close when save button is pressed with valid title', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
const newTitle = 'New Playbook Run Name';
|
||||
|
||||
// Set title
|
||||
act(() => {
|
||||
fireEvent.changeText(input, newTitle);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith(newTitle);
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should trim title when saving', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
const titleWithSpaces = ' New Playbook Run Name ';
|
||||
|
||||
// Set title with spaces
|
||||
act(() => {
|
||||
fireEvent.changeText(input, titleWithSpaces);
|
||||
});
|
||||
|
||||
// Trigger save button press
|
||||
const saveCallback = (useNavButtonPressed as any).lastCallback;
|
||||
act(() => {
|
||||
saveCallback();
|
||||
});
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith('New Playbook Run Name');
|
||||
expect(mockOnSave).not.toHaveBeenCalledWith(titleWithSpaces);
|
||||
});
|
||||
|
||||
it('should close when Android back button is pressed', () => {
|
||||
const props = getBaseProps();
|
||||
renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
// Trigger back handler
|
||||
const backCallback = (useAndroidHardwareBackHandler as any).lastCallback;
|
||||
act(() => {
|
||||
backCallback();
|
||||
});
|
||||
|
||||
expect(Keyboard.dismiss).toHaveBeenCalled();
|
||||
expect(popTopScreen).toHaveBeenCalledWith(componentId);
|
||||
expect(mockOnSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update navigation button when canSave changes', () => {
|
||||
const props = getBaseProps();
|
||||
const {getByTestId} = renderWithIntlAndTheme(<RenamePlaybookRunBottomSheet {...props}/>);
|
||||
|
||||
const input = getByTestId('playbooks.playbook_run.rename.input');
|
||||
|
||||
// Initially disabled (same as currentTitle)
|
||||
expect(setButtons).toHaveBeenCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: false}],
|
||||
});
|
||||
|
||||
// Enable by changing to different text
|
||||
act(() => {
|
||||
fireEvent.changeText(input, 'Different Title');
|
||||
});
|
||||
|
||||
// Should update with enabled button
|
||||
expect(setButtons).toHaveBeenLastCalledWith(componentId, {
|
||||
rightButtons: [{...mockRightButton, enabled: true}],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
|||
import {fetchPlaybookRun, fetchPlaybookRunMetadata, postStatusUpdate} from '@playbooks/actions/remote/runs';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
import {getLastCall} from '@test/mock_helpers';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import PostUpdate from './post_update';
|
||||
|
|
@ -79,11 +80,6 @@ jest.mock('react-native', () => {
|
|||
};
|
||||
});
|
||||
|
||||
function getLastCall<T, U extends any[], V>(mock: jest.Mock<T, U, V>): U {
|
||||
const allCalls = mock.mock.calls;
|
||||
return allCalls[allCalls.length - 1];
|
||||
}
|
||||
|
||||
describe('PostUpdate', () => {
|
||||
function getBaseProps(): ComponentProps<typeof PostUpdate> {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
|||
import {createPlaybookRun} from '@playbooks/actions/remote/runs';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
|
||||
import {getLastCall, getLastCallForButton} from '@test/mock_helpers';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import StartARun from './start_a_run';
|
||||
|
|
@ -70,17 +71,6 @@ jest.mock('@utils/snack_bar', () => ({
|
|||
showPlaybookErrorSnackbar: jest.fn(),
|
||||
}));
|
||||
|
||||
function getLastCall<T, U extends unknown[], V>(mock: jest.Mock<T, U, V>): U {
|
||||
const allCalls = mock.mock.calls;
|
||||
return allCalls[allCalls.length - 1];
|
||||
}
|
||||
|
||||
function getLastCallForButton<T, U extends unknown[], V>(mock: jest.Mock<T, U, V>, buttonId: string): U {
|
||||
const allCalls = mock.mock.calls;
|
||||
const buttonCalls = allCalls.filter((call) => call[0] === buttonId);
|
||||
return buttonCalls[buttonCalls.length - 1];
|
||||
}
|
||||
|
||||
describe('StartARun', () => {
|
||||
function getBaseProps(): ComponentProps<typeof StartARun> {
|
||||
return {
|
||||
|
|
|
|||
26
test/mock_helpers.ts
Normal file
26
test/mock_helpers.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* Get the last call made to a Jest mock function.
|
||||
* @param mock - The Jest mock function
|
||||
* @returns The arguments array from the last call
|
||||
*/
|
||||
export function getLastCall<T, U extends unknown[], V>(mock: jest.Mock<T, U, V>): U {
|
||||
const allCalls = mock.mock.calls;
|
||||
return allCalls[allCalls.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last call made to a Jest mock function for a specific button ID.
|
||||
* Useful for testing navigation button handlers where the first argument is the button ID.
|
||||
* @param mock - The Jest mock function
|
||||
* @param buttonId - The button ID to filter by
|
||||
* @returns The arguments array from the last call matching the button ID
|
||||
*/
|
||||
export function getLastCallForButton<T, U extends unknown[], V>(mock: jest.Mock<T, U, V>, buttonId: string): U {
|
||||
const allCalls = mock.mock.calls;
|
||||
const buttonCalls = allCalls.filter((call) => call[0] === buttonId);
|
||||
return buttonCalls[buttonCalls.length - 1];
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue