|
@@ -0,0 +1,313 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Test: WekanCreator import with swimlane preservation
|
|
|
|
|
+ *
|
|
|
|
|
+ * Simulates exporting a board with swimlanes and importing it back,
|
|
|
|
|
+ * verifying that:
|
|
|
|
|
+ * 1. Swimlanes are correctly mapped from old IDs to new IDs
|
|
|
|
|
+ * 2. Cards reference the correct swimlane IDs after import
|
|
|
|
|
+ * 3. A default swimlane is created when no swimlanes exist
|
|
|
|
|
+ * 4. ID normalization (id → _id) works for all exported items
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+// Mock data: exported board with swimlanes and cards
|
|
|
|
|
+const mockExportedBoard = {
|
|
|
|
|
+ _format: 'wekan-board-1.0.0',
|
|
|
|
|
+ _id: 'board1',
|
|
|
|
|
+ title: 'Test Board',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ color: 'bgnone',
|
|
|
|
|
+ permission: 'private',
|
|
|
|
|
+ createdAt: new Date().toISOString(),
|
|
|
|
|
+ modifiedAt: new Date().toISOString(),
|
|
|
|
|
+ members: [
|
|
|
|
|
+ {
|
|
|
|
|
+ userId: 'user1',
|
|
|
|
|
+ wekanId: 'user1',
|
|
|
|
|
+ isActive: true,
|
|
|
|
|
+ isAdmin: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ labels: [],
|
|
|
|
|
+ swimlanes: [
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'swimlane1',
|
|
|
|
|
+ title: 'Swimlane 1',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'swimlane2',
|
|
|
|
|
+ title: 'Swimlane 2',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ sort: 1,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ lists: [
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'list1',
|
|
|
|
|
+ title: 'To Do',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'list2',
|
|
|
|
|
+ title: 'Done',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ sort: 1,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ cards: [
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'card1',
|
|
|
|
|
+ title: 'Card in swimlane 1',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ swimlaneId: 'swimlane1',
|
|
|
|
|
+ listId: 'list1',
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ description: 'Test card',
|
|
|
|
|
+ dateLastActivity: new Date().toISOString(),
|
|
|
|
|
+ labelIds: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'card2',
|
|
|
|
|
+ title: 'Card in swimlane 2',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ swimlaneId: 'swimlane2',
|
|
|
|
|
+ listId: 'list2',
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ description: 'Another test card',
|
|
|
|
|
+ dateLastActivity: new Date().toISOString(),
|
|
|
|
|
+ labelIds: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ comments: [],
|
|
|
|
|
+ activities: [
|
|
|
|
|
+ {
|
|
|
|
|
+ activityType: 'createBoard',
|
|
|
|
|
+ createdAt: new Date().toISOString(),
|
|
|
|
|
+ userId: 'user1',
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ checklists: [],
|
|
|
|
|
+ checklistItems: [],
|
|
|
|
|
+ subtaskItems: [],
|
|
|
|
|
+ customFields: [],
|
|
|
|
|
+ rules: [],
|
|
|
|
|
+ triggers: [],
|
|
|
|
|
+ actions: [],
|
|
|
|
|
+ users: [
|
|
|
|
|
+ {
|
|
|
|
|
+ _id: 'user1',
|
|
|
|
|
+ username: 'admin',
|
|
|
|
|
+ profile: {
|
|
|
|
|
+ fullname: 'Admin User',
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Export format variation: using 'id' instead of '_id'
|
|
|
|
|
+const mockExportedBoardWithIdField = {
|
|
|
|
|
+ ...mockExportedBoard,
|
|
|
|
|
+ swimlanes: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'swimlane1',
|
|
|
|
|
+ title: 'Swimlane 1 (id variant)',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ lists: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'list1',
|
|
|
|
|
+ title: 'To Do (id variant)',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ cards: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'card1',
|
|
|
|
|
+ title: 'Card (id variant)',
|
|
|
|
|
+ archived: false,
|
|
|
|
|
+ swimlaneId: 'swimlane1',
|
|
|
|
|
+ listId: 'list1',
|
|
|
|
|
+ sort: 0,
|
|
|
|
|
+ description: 'Test card with id field',
|
|
|
|
|
+ dateLastActivity: new Date().toISOString(),
|
|
|
|
|
+ labelIds: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Test: Verify id → _id normalization
|
|
|
|
|
+function testIdNormalization() {
|
|
|
|
|
+ console.log('\n=== Test: ID Normalization (id → _id) ===');
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate the normalization logic from WekanCreator constructor
|
|
|
|
|
+ const normalizeIds = arr => {
|
|
|
|
|
+ if (!arr) return;
|
|
|
|
|
+ arr.forEach(item => {
|
|
|
|
|
+ if (item && item.id && !item._id) {
|
|
|
|
|
+ item._id = item.id;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const testData = {
|
|
|
|
|
+ lists: mockExportedBoardWithIdField.lists,
|
|
|
|
|
+ cards: mockExportedBoardWithIdField.cards,
|
|
|
|
|
+ swimlanes: mockExportedBoardWithIdField.swimlanes,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ normalizeIds(testData.lists);
|
|
|
|
|
+ normalizeIds(testData.cards);
|
|
|
|
|
+ normalizeIds(testData.swimlanes);
|
|
|
|
|
+
|
|
|
|
|
+ // Check results
|
|
|
|
|
+ if (testData.swimlanes[0]._id === 'swimlane1') {
|
|
|
|
|
+ console.log('✓ Swimlane: id → _id normalization created _id');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('✗ Swimlane: id → _id normalization FAILED');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (testData.lists[0]._id === 'list1') {
|
|
|
|
|
+ console.log('✓ List: id → _id normalization created _id');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('✗ List: id → _id normalization FAILED');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (testData.cards[0]._id === 'card1') {
|
|
|
|
|
+ console.log('✓ Card: id → _id normalization created _id');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('✗ Card: id → _id normalization FAILED');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Test: Verify swimlane mapping during import
|
|
|
|
|
+function testSwimlaneMapping() {
|
|
|
|
|
+ console.log('\n=== Test: Swimlane Mapping (export → import) ===');
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate WekanCreator swimlane mapping
|
|
|
|
|
+ const swimlanes = {};
|
|
|
|
|
+ const swimlaneIndexMap = {}; // Track old → new ID mappings
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate createSwimlanes: build mapping of old ID → new ID
|
|
|
|
|
+ mockExportedBoard.swimlanes.forEach((swimlane, index) => {
|
|
|
|
|
+ const oldId = swimlane._id;
|
|
|
|
|
+ const newId = `new_swimlane_${index}`; // Simulated new ID
|
|
|
|
|
+ swimlanes[oldId] = newId;
|
|
|
|
|
+ swimlaneIndexMap[oldId] = newId;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`✓ Created mapping for ${Object.keys(swimlanes).length} swimlanes:`);
|
|
|
|
|
+ Object.entries(swimlanes).forEach(([oldId, newId]) => {
|
|
|
|
|
+ console.log(` ${oldId} → ${newId}`);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate createCards: cards reference swimlanes using mapping
|
|
|
|
|
+ const cardSwimlaneCheck = mockExportedBoard.cards.every(card => {
|
|
|
|
|
+ const oldSwimlaneId = card.swimlaneId;
|
|
|
|
|
+ const newSwimlaneId = swimlanes[oldSwimlaneId];
|
|
|
|
|
+ return newSwimlaneId !== undefined;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (cardSwimlaneCheck) {
|
|
|
|
|
+ console.log('✓ All cards can be mapped to swimlanes');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('✗ Some cards have missing swimlane mappings');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Test: Verify default swimlane creation when none exist
|
|
|
|
|
+function testDefaultSwimlaneCreation() {
|
|
|
|
|
+ console.log('\n=== Test: Default Swimlane Creation ===');
|
|
|
|
|
+
|
|
|
|
|
+ const boardWithoutSwimlanes = {
|
|
|
|
|
+ ...mockExportedBoard,
|
|
|
|
|
+ swimlanes: [],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate the default swimlane logic from WekanCreator
|
|
|
|
|
+ let swimlanes = {};
|
|
|
|
|
+ let defaultSwimlaneId = null;
|
|
|
|
|
+
|
|
|
|
|
+ // If no swimlanes were provided, create a default
|
|
|
|
|
+ if (!swimlanes || Object.keys(swimlanes).length === 0) {
|
|
|
|
|
+ defaultSwimlaneId = 'new_default_swimlane';
|
|
|
|
|
+ console.log('✓ Default swimlane created:', defaultSwimlaneId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify cards without swimlane references use the default
|
|
|
|
|
+ const cardsWithoutSwimlane = boardWithoutSwimlanes.cards.filter(c => !c.swimlaneId);
|
|
|
|
|
+ if (cardsWithoutSwimlane.length > 0 && defaultSwimlaneId) {
|
|
|
|
|
+ console.log(`✓ ${cardsWithoutSwimlane.length} cards will use default swimlane`);
|
|
|
|
|
+ } else if (cardsWithoutSwimlane.length === 0) {
|
|
|
|
|
+ console.log('✓ No cards lacking swimlane (test data all have swimlaneId)');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Test: Verify swimlane + card integrity after full import cycle
|
|
|
|
|
+function testFullImportCycle() {
|
|
|
|
|
+ console.log('\n=== Test: Full Import Cycle ===');
|
|
|
|
|
+
|
|
|
|
|
+ // Step 1: Normalize IDs
|
|
|
|
|
+ const normalizeIds = arr => {
|
|
|
|
|
+ if (!arr) return;
|
|
|
|
|
+ arr.forEach(item => {
|
|
|
|
|
+ if (item && item.id && !item._id) {
|
|
|
|
|
+ item._id = item.id;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const data = JSON.parse(JSON.stringify(mockExportedBoard)); // Deep copy
|
|
|
|
|
+ normalizeIds(data.swimlanes);
|
|
|
|
|
+ normalizeIds(data.lists);
|
|
|
|
|
+ normalizeIds(data.cards);
|
|
|
|
|
+
|
|
|
|
|
+ // Step 2: Map swimlanes
|
|
|
|
|
+ const swimlaneMap = {};
|
|
|
|
|
+ data.swimlanes.forEach((s, idx) => {
|
|
|
|
|
+ swimlaneMap[s._id] = `imported_swimlane_${idx}`;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Step 3: Verify cards get mapped swimlane IDs
|
|
|
|
|
+ let unmappedCards = 0;
|
|
|
|
|
+ data.cards.forEach(card => {
|
|
|
|
|
+ if (card.swimlaneId && !swimlaneMap[card.swimlaneId]) {
|
|
|
|
|
+ unmappedCards++;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (unmappedCards === 0) {
|
|
|
|
|
+ console.log('✓ All cards have valid swimlane references');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(`✗ ${unmappedCards} cards have unmapped swimlane references`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (data.swimlanes.length > 0) {
|
|
|
|
|
+ console.log(`✓ ${data.swimlanes.length} swimlanes preserved in import`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (data.cards.length > 0) {
|
|
|
|
|
+ console.log(`✓ ${data.cards.length} cards preserved in import`);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Run all tests
|
|
|
|
|
+if (typeof describe === 'undefined') {
|
|
|
|
|
+ // Running in Node.js or standalone (not Mocha)
|
|
|
|
|
+ console.log('====================================');
|
|
|
|
|
+ console.log('WekanCreator Import Tests');
|
|
|
|
|
+ console.log('====================================');
|
|
|
|
|
+
|
|
|
|
|
+ testIdNormalization();
|
|
|
|
|
+ testSwimlaneMapping();
|
|
|
|
|
+ testDefaultSwimlaneCreation();
|
|
|
|
|
+ testFullImportCycle();
|
|
|
|
|
+
|
|
|
|
|
+ console.log('\n====================================');
|
|
|
|
|
+ console.log('Tests completed');
|
|
|
|
|
+ console.log('====================================\n');
|
|
|
|
|
+}
|