diff --git a/src/apis/html-page-api.js b/src/apis/html-page-api.js index 84866d7..c964daf 100644 --- a/src/apis/html-page-api.js +++ b/src/apis/html-page-api.js @@ -183,6 +183,45 @@ class HTMLPageAPI { return this._sendDelete(url, data); } + sendNotification(page_id, notification_data, preview_table_config) { + const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/notification/`; + const data = { ...notification_data, page_id }; + if (preview_table_config && typeof preview_table_config === 'object') { + data.preview_table_config = preview_table_config; + } + return this.req.post(url, data); + } + + sendEmail(page_id, email_data, preview_table_config) { + const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/email/`; + const data = { ...email_data, page_id }; + if (preview_table_config && typeof preview_table_config === 'object') { + data.preview_table_config = preview_table_config; + } + return this.req.post(url, data); + } + + getMessageStatus(task_id) { + const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/dtable-message-status/`; + return this.req.get(url, { params: { task_id } }); + } + + runScript(script_name, page_id, table_name, row_id, preview_table_config) { + const encodedScriptName = encodeURIComponent(script_name); + const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/run-script/${encodedScriptName}/`; + const data = { page_id, table_name, row_id }; + if (preview_table_config && typeof preview_table_config === 'object') { + data.preview_table_config = preview_table_config; + } + return this.req.post(url, data); + } + + getScriptResult(script_name, task_id, page_id) { + const encodedScriptName = encodeURIComponent(script_name); + const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/run-script/${encodedScriptName}/result/${task_id}/`; + return this.req.get(url, { params: { page_id } }); + } + getUploadLink(page_id, upload_type = 'file') { const url = `${this.server}api/v2.1/universal-apps/${this.appUuid}/html-page-upload-link/`; return this.req.get(url, { params: { page_id, upload_type } }); diff --git a/src/sdk.js b/src/sdk.js index 175d30e..a210005 100644 --- a/src/sdk.js +++ b/src/sdk.js @@ -163,6 +163,65 @@ export class HTMLPageSDK { return this.htmlPageAPI.deleteLinks(this.options.pageId, tableName, linksData, previewTableConfig); } + sendNotification({ tableName, rowId, emails, msg }) { + const previewTableConfig = this._getPreviewTableConfig({ tableName }); + return this.htmlPageAPI.sendNotification( + this.options.pageId, + { table_name: tableName, row_id: rowId, emails, msg }, + previewTableConfig, + ); + } + + sendEmail({ + tableName, + rowId, + accountName, + sendTo, + copyTo, + replyTo, + subject, + message, + htmlMessage, + attachmentColumnNames, + }) { + const previewTableConfig = this._getPreviewTableConfig({ tableName }); + return this.htmlPageAPI.sendEmail( + this.options.pageId, + { + table_name: tableName, + row_id: rowId, + account_name: accountName, + send_to: sendTo, + copy_to: copyTo, + reply_to: replyTo, + subject, + message, + html_message: htmlMessage, + attachment_column_names: attachmentColumnNames, + }, + previewTableConfig, + ); + } + + getMessageStatus({ taskId }) { + return this.htmlPageAPI.getMessageStatus(taskId); + } + + runScript({ scriptName, tableName, rowId }) { + const previewTableConfig = this._getPreviewTableConfig({ tableName }); + return this.htmlPageAPI.runScript( + scriptName, + this.options.pageId, + tableName, + rowId, + previewTableConfig, + ); + } + + getScriptResult({ scriptName, taskId }) { + return this.htmlPageAPI.getScriptResult(scriptName, taskId, this.options.pageId); + } + uploadFile({ file }) { return this.htmlPageAPI.upload(this.options.pageId, file); } diff --git a/tests/api-action.test.js b/tests/api-action.test.js new file mode 100644 index 0000000..ec2b8b3 --- /dev/null +++ b/tests/api-action.test.js @@ -0,0 +1,173 @@ +import axios from 'axios'; +import HTMLPageAPI from '../src/apis/html-page-api'; + +jest.mock('axios', () => ({ + __esModule: true, + default: { + create: jest.fn(), + get: jest.fn(), + post: jest.fn(), + }, +})); + +function createApi() { + const get = jest.fn(); + const post = jest.fn(); + axios.create.mockReturnValue({ get, post }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + return { api, get, post }; +} + +describe('HTMLPageAPI action operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('sends notification and email requests with public action parameters', () => { + const { api, post } = createApi(); + const notificationResponse = { data: { task_id: 'notification-task-1' } }; + const emailResponse = { data: { task_id: 'email-task-1' } }; + post.mockReturnValueOnce(notificationResponse).mockReturnValueOnce(emailResponse); + + const notificationResult = api.sendNotification('page-1', { + table_name: 'Products', + row_id: 'row-1', + emails: ['recipient@example.com'], + msg: 'hello {Name}', + }); + const emailResult = api.sendEmail('page-1', { + table_name: 'Products', + row_id: 'row-1', + account_name: 'Operations SMTP', + send_to: ['recipient@example.com'], + copy_to: ['audit@example.com'], + reply_to: ['service@example.com'], + subject: 'Order {Order number}', + html_message: '
Hello
', + attachment_column_names: ['Attachment'], + }); + + expect(notificationResult).toBe(notificationResponse); + expect(emailResult).toBe(emailResponse); + expect(post).toHaveBeenNthCalledWith( + 1, + 'https://example.com/api/v2.1/universal-apps/app-uuid/notification/', + { + page_id: 'page-1', + table_name: 'Products', + row_id: 'row-1', + emails: ['recipient@example.com'], + msg: 'hello {Name}', + }, + ); + expect(post).toHaveBeenNthCalledWith( + 2, + 'https://example.com/api/v2.1/universal-apps/app-uuid/email/', + { + page_id: 'page-1', + table_name: 'Products', + row_id: 'row-1', + account_name: 'Operations SMTP', + send_to: ['recipient@example.com'], + copy_to: ['audit@example.com'], + reply_to: ['service@example.com'], + subject: 'Order {Order number}', + html_message: 'Hello
', + attachment_column_names: ['Attachment'], + }, + ); + }); + + it('sends preview_table_config with table-scoped action requests', () => { + const { api, post } = createApi(); + const previewTableConfig = { table_id: 'table-1', permissions: {} }; + + api.sendNotification('ai_agent', { + table_name: 'Products', + row_id: 'row-1', + emails: ['recipient@example.com'], + msg: 'hello', + }, previewTableConfig); + api.sendEmail('ai_agent', { + table_name: 'Products', + row_id: 'row-1', + account_name: 'Operations SMTP', + send_to: ['recipient@example.com'], + subject: 'Hello', + message: 'Hello', + }, previewTableConfig); + api.runScript('Generate report', 'ai_agent', 'Products', 'row-1', previewTableConfig); + + expect(post).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ + page_id: 'ai_agent', + preview_table_config: previewTableConfig, + })); + expect(post).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ + page_id: 'ai_agent', + preview_table_config: previewTableConfig, + })); + expect(post).toHaveBeenNthCalledWith(3, expect.any(String), expect.objectContaining({ + page_id: 'ai_agent', + preview_table_config: previewTableConfig, + })); + }); + + it('gets message status by task ID', () => { + const { api, get } = createApi(); + const response = { data: { status: 'SUCCESS' } }; + get.mockReturnValue(response); + + const result = api.getMessageStatus('message-task-1'); + + expect(result).toBe(response); + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/dtable-message-status/', + { params: { task_id: 'message-task-1' } }, + ); + }); + + it('runs and gets a script by its display name', () => { + const { api, get, post } = createApi(); + const runResponse = { data: { task_id: 42 } }; + const resultResponse = { + data: { + script: { + state: 'finished', + success: false, + output: 'Script execution failed', + return_code: 125, + }, + }, + }; + post.mockReturnValue(runResponse); + get.mockReturnValue(resultResponse); + + const runResult = api.runScript('Generate / report', 'page-1'); + const result = api.getScriptResult('Generate / report', 42, 'page-1'); + + expect(runResult).toBe(runResponse); + expect(result).toBe(resultResponse); + expect(result.data.script).toEqual({ + state: 'finished', + success: false, + output: 'Script execution failed', + return_code: 125, + }); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/run-script/Generate%20%2F%20report/', + { page_id: 'page-1', table_name: undefined, row_id: undefined }, + ); + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/run-script/Generate%20%2F%20report/result/42/', + { params: { page_id: 'page-1' } }, + ); + }); +}); + diff --git a/tests/api-links.test.js b/tests/api-links.test.js new file mode 100644 index 0000000..3b72c57 --- /dev/null +++ b/tests/api-links.test.js @@ -0,0 +1,201 @@ +import axios from 'axios'; +import HTMLPageAPI from '../src/apis/html-page-api'; + +jest.mock('axios', () => ({ + __esModule: true, + default: { + create: jest.fn(), + get: jest.fn(), + post: jest.fn(), + }, +})); + +function createApi() { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const del = jest.fn(); + axios.create.mockReturnValue({ + get, + post, + put, + delete: del, + }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + return { api, get, post, put, del }; +} + +describe('HTMLPageAPI link operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('adds a link and returns the updated row', () => { + const { api, post } = createApi(); + const updatedRow = { + _id: 'task-row-1', + 'Related projects': [ + { row_id: 'project-row-1', display_value: 'Project 1' }, + ], + }; + const response = { data: { success: true, row: updatedRow } }; + post.mockReturnValue(response); + + const result = api.addLink( + 'page-1', + 'Tasks', + 'task-row-1', + 'Related projects', + 'project-row-1', + ); + + expect(result).toBe(response); + expect(result.data.row).toEqual(updatedRow); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/', + { + page_id: 'page-1', + table_name: 'Tasks', + row_id: 'task-row-1', + link_column_name: 'Related projects', + other_row_id: 'project-row-1', + }, + ); + }); + + it('deletes a link and returns the updated row', () => { + const { api, del } = createApi(); + const updatedRow = { _id: 'task-row-1', 'Related projects': [] }; + const response = { data: { success: true, row: updatedRow } }; + del.mockReturnValue(response); + + const result = api.deleteLink( + 'page-1', + 'Tasks', + 'task-row-1', + 'Related projects', + 'project-row-1', + ); + + expect(result).toBe(response); + expect(result.data.row).toEqual(updatedRow); + expect(del).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/', + { + headers: { 'Content-Type': 'application/json' }, + data: { + page_id: 'page-1', + table_name: 'Tasks', + row_id: 'task-row-1', + link_column_name: 'Related projects', + other_row_id: 'project-row-1', + }, + }, + ); + }); + + it('adds and deletes links in batch and returns the updated rows', () => { + const { api, post, del } = createApi(); + const linksData = [ + { + row_id: 'task-row-1', + links: { + 'Related projects': ['project-row-1', 'project-row-2'], + }, + }, + ]; + const addedRows = [{ + _id: 'task-row-1', + 'Related projects': [ + { row_id: 'project-row-1', display_value: 'Project 1' }, + { row_id: 'project-row-2', display_value: 'Project 2' }, + ], + }]; + const deletedRows = [{ _id: 'task-row-1', 'Related projects': [] }]; + const addResponse = { data: { success: true, rows: addedRows } }; + const deleteResponse = { data: { success: true, rows: deletedRows } }; + post.mockReturnValue(addResponse); + del.mockReturnValue(deleteResponse); + + const addResult = api.addLinks('page-1', 'Tasks', linksData); + const deleteResult = api.deleteLinks('page-1', 'Tasks', linksData); + + expect(addResult).toBe(addResponse); + expect(addResult.data.rows).toEqual(addedRows); + expect(deleteResult).toBe(deleteResponse); + expect(deleteResult.data.rows).toEqual(deletedRows); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/batch/', + { + page_id: 'page-1', + table_name: 'Tasks', + links_data: linksData, + }, + ); + expect(del).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/batch/', + { + headers: { 'Content-Type': 'application/json' }, + data: { + page_id: 'page-1', + table_name: 'Tasks', + links_data: linksData, + }, + }, + ); + }); + + it('includes preview_table_config in link requests for ai_agent preview', () => { + const { api, post, del } = createApi(); + const previewTableConfig = { table_id: 'table-1', permissions: {} }; + const linksData = [{ row_id: 'task-row-1', links: { related: ['project-row-1'] } }]; + + api.addLink('ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', previewTableConfig); + api.deleteLink('ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', previewTableConfig); + api.addLinks('ai_agent', 'Tasks', linksData, previewTableConfig); + api.deleteLinks('ai_agent', 'Tasks', linksData, previewTableConfig); + + expect(post).toHaveBeenNthCalledWith(1, expect.any(String), { + page_id: 'ai_agent', + table_name: 'Tasks', + row_id: 'task-row-1', + link_column_name: 'related', + other_row_id: 'project-row-1', + preview_table_config: previewTableConfig, + }); + expect(del).toHaveBeenNthCalledWith(1, expect.any(String), { + headers: { 'Content-Type': 'application/json' }, + data: { + page_id: 'ai_agent', + table_name: 'Tasks', + row_id: 'task-row-1', + link_column_name: 'related', + other_row_id: 'project-row-1', + preview_table_config: previewTableConfig, + }, + }); + expect(post).toHaveBeenNthCalledWith(2, expect.any(String), { + page_id: 'ai_agent', + table_name: 'Tasks', + links_data: linksData, + preview_table_config: previewTableConfig, + }); + expect(del).toHaveBeenNthCalledWith(2, expect.any(String), { + headers: { 'Content-Type': 'application/json' }, + data: { + page_id: 'ai_agent', + table_name: 'Tasks', + links_data: linksData, + preview_table_config: previewTableConfig, + }, + }); + }); +}); + diff --git a/tests/api-rows.test.js b/tests/api-rows.test.js new file mode 100644 index 0000000..8dfae73 --- /dev/null +++ b/tests/api-rows.test.js @@ -0,0 +1,434 @@ +import axios from 'axios'; +import HTMLPageAPI from '../src/apis/html-page-api'; + +jest.mock('axios', () => ({ + __esModule: true, + default: { + create: jest.fn(), + get: jest.fn(), + post: jest.fn(), + }, +})); + +function createApi() { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const del = jest.fn(); + axios.create.mockReturnValue({ + get, + post, + put, + delete: del, + }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + return { api, get, post, put, del }; +} + +describe('HTMLPageAPI.queryRows', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('queryRows posts table name and conditions to the query endpoint', () => { + const { api, post } = createApi(); + + const response = { data: { metadata: [], results: [] } }; + post.mockReturnValue(response); + + const conditions = [{ columnName: 'Phone', value: '13800138000' }]; + const result = api.queryRows('page-1', 'Orders', conditions, 0, 100); + + expect(result).toEqual(response); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/query/', + { + page_id: 'page-1', + table_name: 'Orders', + conditions, + start: 0, + limit: 100, + }, + ); + }); + + it('queryRows sends preview_table_config for ai_agent preview', () => { + const { api, post } = createApi(); + const previewTableConfig = { + table_id: 'tbl-1', + permissions: { + query_rows_permission: { + enabled: true, + columns_keys: ['phone'], + query_columns: [{ column_key: 'phone', enable_fuzzy_query: true }], + }, + }, + }; + + api.queryRows('ai_agent', 'Orders', [{ columnName: 'Phone', value: '138' }], 0, 20, previewTableConfig); + + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/query/', + { + page_id: 'ai_agent', + table_name: 'Orders', + conditions: [{ columnName: 'Phone', value: '138' }], + start: 0, + limit: 20, + preview_table_config: previewTableConfig, + }, + ); + }); +}); + +describe('HTMLPageAPI.listRows', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('listRows with start and limit', () => { + const get = jest.fn(); + axios.create.mockReturnValue({ get }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + const response = { data: { rows: [{ _id: 'row-1', '0000': 'John' }] } }; + get.mockReturnValue(response); + + const result = api.listRows('page-1', 'TableName', 0, 100); + + expect(result).toEqual(response); + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + params: { + page_id: 'page-1', + table_name: 'TableName', + start: 0, + limit: 100, + }, + }, + ); + }); + + it('listRows without start and limit', () => { + const get = jest.fn(); + axios.create.mockReturnValue({ get }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + const response = { data: { rows: [{ _id: 'row-1', '0000': 'John' }] } }; + get.mockReturnValue(response); + + const result = api.listRows('page-1', 'TableName'); + + expect(result).toEqual(response); + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + params: { + page_id: 'page-1', + table_name: 'TableName', + start: undefined, + limit: undefined, + }, + }, + ); + }); + + it('listRows serializes preview_table_config', () => { + const { api, get } = createApi(); + const previewTableConfig = { table_id: 'tbl-1', permissions: {} }; + + api.listRows('ai_agent', 'TableName', 0, 100, previewTableConfig); + + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + params: { + page_id: 'ai_agent', + table_name: 'TableName', + start: 0, + limit: 100, + preview_table_config: JSON.stringify(previewTableConfig), + }, + }, + ); + }); +}); + +describe('HTMLPageAPI.addRow(s)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('addRow', () => { + const { api, post } = createApi(); + const rowData = { Name: 'John' }; + const newRow = { '0000': 'John', _id: 'row-1' }; + const response = { data: { success: true, row: newRow } }; + post.mockReturnValue(response); + + const result = api.addRow('page-1', 'TableName', rowData); + expect(result).toEqual(response); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + page_id: 'page-1', + table_name: 'TableName', + row_data: rowData, + }, + ); + }); + + it('addRows', () => { + const { api, post } = createApi(); + const rowsData = [{ Name: 'John' }, { Name: 'Jane' }]; + const newRows = [{ '0000': 'John', _id: 'row-1' }, { '0000': 'Jane', _id: 'row-2' }]; + const response = { data: { success: true }, rows: newRows, inserted_row_count: 2 }; + post.mockReturnValue(response); + + const result = api.addRows('page-1', 'TableName', rowsData); + + expect(result).toEqual(response); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/', + { + page_id: 'page-1', + table_name: 'TableName', + rows_data: rowsData, + }, + ); + }); +}); + +describe('HTMLPageAPI.updateRow(s)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('updateRow', () => { + const { api, put } = createApi(); + const rowId = 'row-1'; + const rowData = { Name: 'Jane' }; + const newRow = { '0000': 'Jane', _id: 'row-1' }; + const response = { data: { success: true, row: newRow } }; + put.mockReturnValue(response); + const result = api.updateRow('page-1', 'TableName', rowId, rowData); + + expect(result).toEqual(response); + expect(put).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + page_id: 'page-1', + table_name: 'TableName', + row_id: rowId, + row_data: rowData, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); + }); + + it('updateRows', () => { + const { api, put } = createApi(); + const rowsData = [{ row_id: 'row-1', Name: 'Jane' }]; + const response = { + data: { + success: true, + rows: [{ _id: 'row-1', '0000': 'Jane' }], + }, + }; + put.mockReturnValue(response); + + const result = api.updateRows('page-1', 'TableName', rowsData); + + expect(result).toEqual(response); + + expect(put).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/', + { + page_id: 'page-1', + table_name: 'TableName', + rows_data: rowsData, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); + }); + + it('updateRow sends link-column row ids and returns expanded link values', () => { + const { api, put } = createApi(); + const rowData = { + Name: 'Updated task', + 'Related projects': ['project-row-1', 'project-row-2'], + }; + const updatedRow = { + _id: 'task-row-1', + Name: 'Updated task', + 'Related projects': [ + { row_id: 'project-row-1', display_value: 'Project 1' }, + { row_id: 'project-row-2', display_value: 'Project 2' }, + ], + }; + const response = { data: { success: true, row: updatedRow } }; + put.mockReturnValue(response); + + const result = api.updateRow('page-1', 'Tasks', 'task-row-1', rowData); + + expect(result.data.row).toEqual(updatedRow); + expect(put).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + page_id: 'page-1', + table_name: 'Tasks', + row_id: 'task-row-1', + row_data: rowData, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); + }); + + it('updateRows sends link-column row ids and returns expanded link values', () => { + const { api, put } = createApi(); + const rowsData = [ + { + row_id: 'task-row-1', + row: { + Name: 'Updated task 1', + 'Related projects': ['project-row-1', 'project-row-2'], + }, + }, + { + row_id: 'task-row-2', + row: { + 'Related projects': [], + }, + }, + ]; + const updatedRows = [ + { + _id: 'task-row-1', + Name: 'Updated task 1', + 'Related projects': [ + { row_id: 'project-row-1', display_value: 'Project 1' }, + { row_id: 'project-row-2', display_value: 'Project 2' }, + ], + }, + { + _id: 'task-row-2', + 'Related projects': [], + }, + ]; + const response = { data: { success: true, rows: updatedRows } }; + put.mockReturnValue(response); + + const result = api.updateRows('page-1', 'Tasks', rowsData); + + expect(result.data.rows).toEqual(updatedRows); + expect(put).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/', + { + page_id: 'page-1', + table_name: 'Tasks', + rows_data: rowsData, + }, + { + headers: { 'Content-Type': 'application/json' }, + }, + ); + }); +}); + +describe('HTMLPageAPI.deleteRows(s)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('sends deleteRows requests with row ids', () => { + const { api, del } = createApi(); + del.mockReturnValue({ data: { success: true } }); + + api.deleteRows('page-1', 'TableName', ['row-1']); + + expect(del).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', + { + headers: { 'Content-Type': 'application/json' }, + data: { + page_id: 'page-1', + table_name: 'TableName', + rows_ids: ['row-1'], + }, + }, + ); + }); + + it('write APIs send preview_table_config', () => { + const { api, post, put, del } = createApi(); + const previewTableConfig = { table_id: 'tbl-1', permissions: {} }; + + api.addRow('ai_agent', 'TableName', { Name: 'A' }, previewTableConfig); + api.addRows('ai_agent', 'TableName', [{ Name: 'B' }], previewTableConfig); + api.updateRow('ai_agent', 'TableName', 'row-1', { Name: 'C' }, previewTableConfig); + api.updateRows('ai_agent', 'TableName', [{ row_id: 'row-1', row: { Name: 'D' } }], previewTableConfig); + api.deleteRows('ai_agent', 'TableName', ['row-1'], previewTableConfig); + + expect(post).toHaveBeenNthCalledWith(1, expect.any(String), { + page_id: 'ai_agent', + table_name: 'TableName', + row_data: { Name: 'A' }, + preview_table_config: previewTableConfig, + }); + expect(post).toHaveBeenNthCalledWith(2, expect.any(String), { + page_id: 'ai_agent', + table_name: 'TableName', + rows_data: [{ Name: 'B' }], + preview_table_config: previewTableConfig, + }); + expect(put).toHaveBeenNthCalledWith(1, expect.any(String), { + page_id: 'ai_agent', + table_name: 'TableName', + row_id: 'row-1', + row_data: { Name: 'C' }, + preview_table_config: previewTableConfig, + }, expect.any(Object)); + expect(put).toHaveBeenNthCalledWith(2, expect.any(String), { + page_id: 'ai_agent', + table_name: 'TableName', + rows_data: [{ row_id: 'row-1', row: { Name: 'D' } }], + preview_table_config: previewTableConfig, + }, expect.any(Object)); + expect(del).toHaveBeenCalledWith(expect.any(String), { + headers: { 'Content-Type': 'application/json' }, + data: { + page_id: 'ai_agent', + table_name: 'TableName', + rows_ids: ['row-1'], + preview_table_config: previewTableConfig, + }, + }); + }); +}); + diff --git a/tests/api.test.js b/tests/api.test.js new file mode 100644 index 0000000..cdb0cdf --- /dev/null +++ b/tests/api.test.js @@ -0,0 +1,287 @@ +import axios from 'axios'; +import HTMLPageAPI from '../src/apis/html-page-api'; + +jest.mock('axios', () => ({ + __esModule: true, + default: { + create: jest.fn(), + get: jest.fn(), + post: jest.fn(), + }, +})); + +function createApi() { + const get = jest.fn(); + const post = jest.fn(); + const put = jest.fn(); + const del = jest.fn(); + axios.create.mockReturnValue({ + get, + post, + put, + delete: del, + }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + return { api, get, post, put, del }; +} + +describe('HTMLPageAPI.listCollaborators', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('lists collaborators', () => { + const { api, get } = createApi(); + const response = { + data: { + collaborator_list: [{ email: 'user@example.com', name: 'User' }], + }, + }; + get.mockReturnValue(response); + + const result = api.listCollaborators(); + + expect(result).toEqual(response); + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-collaborators/', + ); + }); + +}); + +describe('HTMLPageAPI.getCurrentUser', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('gets the current user', () => { + const { api, get } = createApi(); + const response = { + data: { + username: 'user@example.com', + name: 'User', + user_id: 'EMP-001', + avatar_url: 'https://example.com/avatar.png', + role_id: 7, + }, + }; + get.mockReturnValue(response); + + const result = api.getCurrentUser(); + + expect(result).toEqual(response); + expect(get).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/current-user/', + ); + }); +}); + +describe('HTMLPageAPI.resolveUsers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('resolves users', () => { + const { api, post } = createApi(); + const response = { + data: { + user_list: [{ email: 'user@example.com', name: 'User' }], + }, + }; + post.mockReturnValue(response); + + const result = api.resolveUsers(['user@example.com']); + + expect(result).toEqual(response); + expect(post).toHaveBeenCalledWith( + 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-users/resolve/', + { + user_ids: ['user@example.com'], + }, + ); + }); +}); + +describe('HTMLPageAPI.upload', () => { + beforeEach(() => { + jest.clearAllMocks(); + global.FormData = class { + constructor() { + this.fields = []; + } + + append(key, value, filename) { + this.fields.push([key, value, filename]); + } + }; + }); + + it('gets an upload link and uploads a file', async () => { + const get = jest.fn(); + axios.create.mockReturnValue({ get }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + api.getUploadLink = jest.fn().mockResolvedValue({ + data: { + upload_link: 'https://upload.example.com/upload', + parent_path: '/parent', + file_relative_path: 'files', + img_relative_path: 'images', + asset_parent_url: 'https://assets.example.com', + }, + }); + api.uploadAsset = jest.fn().mockResolvedValue({ + data: [{ name: 'file.txt', size: 12 }], + }); + + const file = { name: 'file.txt' }; + const result = await api.upload('page-1', file); + + expect(api.getUploadLink).toHaveBeenCalledWith('page-1', 'file'); + expect(api.uploadAsset).toHaveBeenCalledWith( + 'https://upload.example.com/upload', + expect.any(FormData), + ); + expect(result).toEqual({ + name: 'file.txt', + size: 12, + type: 'file', + url: 'https://assets.example.com/files/file.txt', + }); + }); + + it('uses image relative path when uploading an image', async () => { + const get = jest.fn(); + axios.create.mockReturnValue({ get }); + + const api = new HTMLPageAPI(); + api.init({ + server: 'https://example.com/', + accessToken: 'token', + appUuid: 'app-uuid', + }); + + api.getUploadLink = jest.fn().mockResolvedValue({ + data: { + upload_link: 'https://upload.example.com/upload', + parent_path: '/parent', + file_relative_path: 'files', + img_relative_path: 'images', + asset_parent_url: 'https://assets.example.com', + }, + }); + api.uploadAsset = jest.fn().mockResolvedValue({ + data: [{ name: 'image.png', size: 34 }], + }); + + const file = { name: 'image.png' }; + const result = await api.upload('page-1', file, 'image'); + + expect(api.getUploadLink).toHaveBeenCalledWith('page-1', 'image'); + expect(result).toEqual({ + name: 'image.png', + size: 34, + type: 'image', + url: 'https://assets.example.com/images/image.png', + }); + }); +}); + +describe('HTMLPageAPI.initWithAccountToken', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('rejects with the access-token request error', async () => { + axios.get.mockRejectedValue(new Error('Request failed with status code 401')); + const api = new HTMLPageAPI(); + + await expect(api.initWithAccountToken({ + server: 'https://custom-app-server.example.com/', + accountToken: 'invalid-account-token', + appUuid: 'app-uuid', + })).rejects.toThrow('Failed to get access token: Request failed with status code 401'); + }); + + it('requires development access-token configuration', async () => { + const api = new HTMLPageAPI(); + + await expect(api.initWithAccountToken({ + server: 'https://custom-app-server.example.com/', + accountToken: 'account-token', + appUuid: '', + })).rejects.toThrow('Failed to get access token: missing server, accountToken, or appUuid'); + expect(axios.get).not.toHaveBeenCalled(); + }); + + it('rejects when the access-token response does not contain an access token', async () => { + axios.get.mockResolvedValue({ data: {} }); + const api = new HTMLPageAPI(); + + const error = await api.initWithAccountToken({ + server: 'https://custom-app-server.example.com/', + accountToken: 'account-token', + appUuid: 'app-uuid', + }).catch(error => error); + + expect(error.message).toBe('Failed to get access token: access_token missing'); + }); +}); + +describe('HTMLPageAPI.getParentOrigin', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('uses the custom-app-server bootstrap endpoint with the app access token', async () => { + axios.post.mockResolvedValue({ data: { parentOrigin: 'https://app.example.com' } }); + const api = new HTMLPageAPI(); + + await expect(api.getParentOrigin({ + server: 'https://custom-app-server.example.com/', + accessToken: 'access-token', + appUuid: 'app-uuid', + })).resolves.toBe('https://app.example.com'); + + expect(axios.post).toHaveBeenCalledWith( + 'https://custom-app-server.example.com/api/v2.1/universal-apps/bootstrap/', + { app_uuid: 'app-uuid' }, + { headers: { Authorization: 'Token access-token' } }, + ); + }); + + it('adds parentOrigin context when the bootstrap request fails', async () => { + axios.post.mockRejectedValue(new Error('Request failed with status code 401')); + const api = new HTMLPageAPI(); + + await expect(api.getParentOrigin({ + server: 'https://custom-app-server.example.com/', + accessToken: 'invalid-access-token', + appUuid: 'app-uuid', + })).rejects.toThrow('Failed to get parentOrigin: Request failed with status code 401'); + }); + + it('requires bootstrap configuration', async () => { + const api = new HTMLPageAPI(); + + await expect(api.getParentOrigin({ + server: 'https://custom-app-server.example.com/', + accessToken: 'access-token', + appUuid: '', + })).rejects.toThrow('Failed to get parentOrigin: missing server, accessToken, or appUuid'); + expect(axios.post).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/html-page-api.test.js b/tests/html-page-api.test.js deleted file mode 100644 index ce3b370..0000000 --- a/tests/html-page-api.test.js +++ /dev/null @@ -1,854 +0,0 @@ -import axios from 'axios'; -import HTMLPageAPI from '../src/apis/html-page-api'; - -jest.mock('axios', () => ({ - __esModule: true, - default: { - create: jest.fn(), - get: jest.fn(), - post: jest.fn(), - }, -})); - -function createApi() { - const get = jest.fn(); - const post = jest.fn(); - const put = jest.fn(); - const del = jest.fn(); - axios.create.mockReturnValue({ - get, - post, - put, - delete: del, - }); - - const api = new HTMLPageAPI(); - api.init({ - server: 'https://example.com/', - accessToken: 'token', - appUuid: 'app-uuid', - }); - - return { api, get, post, put, del }; -} - -describe('HTMLPageAPI.queryRows', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('queryRows posts table name and conditions to the query endpoint', () => { - const { api, post } = createApi(); - - const response = { data: { metadata: [], results: [] } }; - post.mockReturnValue(response); - - const conditions = [{ columnName: 'Phone', value: '13800138000' }]; - const result = api.queryRows('page-1', 'Orders', conditions, 0, 100); - - expect(result).toEqual(response); - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/query/', - { - page_id: 'page-1', - table_name: 'Orders', - conditions, - start: 0, - limit: 100, - }, - ); - }); - - it('queryRows sends preview_table_config for ai_agent preview', () => { - const { api, post } = createApi(); - const previewTableConfig = { - table_id: 'tbl-1', - permissions: { - query_rows_permission: { - enabled: true, - columns_keys: ['phone'], - query_columns: [{ column_key: 'phone', enable_fuzzy_query: true }], - }, - }, - }; - - api.queryRows('ai_agent', 'Orders', [{ columnName: 'Phone', value: '138' }], 0, 20, previewTableConfig); - - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/query/', - { - page_id: 'ai_agent', - table_name: 'Orders', - conditions: [{ columnName: 'Phone', value: '138' }], - start: 0, - limit: 20, - preview_table_config: previewTableConfig, - }, - ); - }); -}); - -describe('HTMLPageAPI.listRows', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('listRows with start and limit', () => { - const get = jest.fn(); - axios.create.mockReturnValue({ get }); - - const api = new HTMLPageAPI(); - api.init({ - server: 'https://example.com/', - accessToken: 'token', - appUuid: 'app-uuid', - }); - - const response = { data: { rows: [{ _id: 'row-1', '0000': 'John' }] } }; - get.mockReturnValue(response); - - const result = api.listRows('page-1', 'TableName', 0, 100); - - expect(result).toEqual(response); - expect(get).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - params: { - page_id: 'page-1', - table_name: 'TableName', - start: 0, - limit: 100, - }, - }, - ); - }); - - it('listRows without start and limit', () => { - const get = jest.fn(); - axios.create.mockReturnValue({ get }); - - const api = new HTMLPageAPI(); - api.init({ - server: 'https://example.com/', - accessToken: 'token', - appUuid: 'app-uuid', - }); - - const response = { data: { rows: [{ _id: 'row-1', '0000': 'John' }] } }; - get.mockReturnValue(response); - - const result = api.listRows('page-1', 'TableName'); - - expect(result).toEqual(response); - expect(get).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - params: { - page_id: 'page-1', - table_name: 'TableName', - start: undefined, - limit: undefined, - }, - }, - ); - }); - - it('listRows serializes preview_table_config', () => { - const { api, get } = createApi(); - const previewTableConfig = { table_id: 'tbl-1', permissions: {} }; - - api.listRows('ai_agent', 'TableName', 0, 100, previewTableConfig); - - expect(get).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - params: { - page_id: 'ai_agent', - table_name: 'TableName', - start: 0, - limit: 100, - preview_table_config: JSON.stringify(previewTableConfig), - }, - }, - ); - }); -}); - -describe('HTMLPageAPI.listCollaborators', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('lists collaborators', () => { - const { api, get } = createApi(); - const response = { - data: { - collaborator_list: [{ email: 'user@example.com', name: 'User' }], - }, - }; - get.mockReturnValue(response); - - const result = api.listCollaborators(); - - expect(result).toEqual(response); - expect(get).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-collaborators/', - ); - }); - -}); - -describe('HTMLPageAPI.getCurrentUser', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('gets the current user', () => { - const { api, get } = createApi(); - const response = { - data: { - username: 'user@example.com', - name: 'User', - user_id: 'EMP-001', - avatar_url: 'https://example.com/avatar.png', - role_id: 7, - }, - }; - get.mockReturnValue(response); - - const result = api.getCurrentUser(); - - expect(result).toEqual(response); - expect(get).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/current-user/', - ); - }); -}); - -describe('HTMLPageAPI.resolveUsers', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('resolves users', () => { - const { api, post } = createApi(); - const response = { - data: { - user_list: [{ email: 'user@example.com', name: 'User' }], - }, - }; - post.mockReturnValue(response); - - const result = api.resolveUsers(['user@example.com']); - - expect(result).toEqual(response); - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-users/resolve/', - { - user_ids: ['user@example.com'], - }, - ); - }); -}); - -describe('HTMLPageAPI.addRow(s)', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('addRow', () => { - const { api, post } = createApi(); - const rowData = { Name: 'John' }; - const newRow = { '0000': 'John', _id: 'row-1' }; - const response = { data: { success: true, row: newRow } }; - post.mockReturnValue(response); - - const result = api.addRow('page-1', 'TableName', rowData); - expect(result).toEqual(response); - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - page_id: 'page-1', - table_name: 'TableName', - row_data: rowData, - }, - ); - }); - - it('addRows', () => { - const { api, post } = createApi(); - const rowsData = [{ Name: 'John' }, { Name: 'Jane' }]; - const newRows = [{ '0000': 'John', _id: 'row-1' }, { '0000': 'Jane', _id: 'row-2' }]; - const response = { data: { success: true }, rows: newRows, inserted_row_count: 2 }; - post.mockReturnValue(response); - - const result = api.addRows('page-1', 'TableName', rowsData); - - expect(result).toEqual(response); - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/', - { - page_id: 'page-1', - table_name: 'TableName', - rows_data: rowsData, - }, - ); - }); -}); - -describe('HTMLPageAPI.updateRow(s)', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('updateRow', () => { - const { api, put } = createApi(); - const rowId = 'row-1'; - const rowData = { Name: 'Jane' }; - const newRow = { '0000': 'Jane', _id: 'row-1' }; - const response = { data: { success: true, row: newRow } }; - put.mockReturnValue(response); - const result = api.updateRow('page-1', 'TableName', rowId, rowData); - - expect(result).toEqual(response); - expect(put).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - page_id: 'page-1', - table_name: 'TableName', - row_id: rowId, - row_data: rowData, - }, - { - headers: { 'Content-Type': 'application/json' }, - }, - ); - }); - - it('updateRows', () => { - const { api, put } = createApi(); - const rowsData = [{ row_id: 'row-1', Name: 'Jane' }]; - const response = { - data: { - success: true, - rows: [{ _id: 'row-1', '0000': 'Jane' }], - }, - }; - put.mockReturnValue(response); - - const result = api.updateRows('page-1', 'TableName', rowsData); - - expect(result).toEqual(response); - - expect(put).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/', - { - page_id: 'page-1', - table_name: 'TableName', - rows_data: rowsData, - }, - { - headers: { 'Content-Type': 'application/json' }, - }, - ); - }); - - it('updateRow sends link-column row ids and returns expanded link values', () => { - const { api, put } = createApi(); - const rowData = { - Name: 'Updated task', - 'Related projects': ['project-row-1', 'project-row-2'], - }; - const updatedRow = { - _id: 'task-row-1', - Name: 'Updated task', - 'Related projects': [ - { row_id: 'project-row-1', display_value: 'Project 1' }, - { row_id: 'project-row-2', display_value: 'Project 2' }, - ], - }; - const response = { data: { success: true, row: updatedRow } }; - put.mockReturnValue(response); - - const result = api.updateRow('page-1', 'Tasks', 'task-row-1', rowData); - - expect(result.data.row).toEqual(updatedRow); - expect(put).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - page_id: 'page-1', - table_name: 'Tasks', - row_id: 'task-row-1', - row_data: rowData, - }, - { - headers: { 'Content-Type': 'application/json' }, - }, - ); - }); - - it('updateRows sends link-column row ids and returns expanded link values', () => { - const { api, put } = createApi(); - const rowsData = [ - { - row_id: 'task-row-1', - row: { - Name: 'Updated task 1', - 'Related projects': ['project-row-1', 'project-row-2'], - }, - }, - { - row_id: 'task-row-2', - row: { - 'Related projects': [], - }, - }, - ]; - const updatedRows = [ - { - _id: 'task-row-1', - Name: 'Updated task 1', - 'Related projects': [ - { row_id: 'project-row-1', display_value: 'Project 1' }, - { row_id: 'project-row-2', display_value: 'Project 2' }, - ], - }, - { - _id: 'task-row-2', - 'Related projects': [], - }, - ]; - const response = { data: { success: true, rows: updatedRows } }; - put.mockReturnValue(response); - - const result = api.updateRows('page-1', 'Tasks', rowsData); - - expect(result.data.rows).toEqual(updatedRows); - expect(put).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/batch/', - { - page_id: 'page-1', - table_name: 'Tasks', - rows_data: rowsData, - }, - { - headers: { 'Content-Type': 'application/json' }, - }, - ); - }); -}); - -describe('HTMLPageAPI.deleteRows(s)', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('sends deleteRows requests with row ids', () => { - const { api, del } = createApi(); - del.mockReturnValue({ data: { success: true } }); - - api.deleteRows('page-1', 'TableName', ['row-1']); - - expect(del).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-rows/', - { - headers: { 'Content-Type': 'application/json' }, - data: { - page_id: 'page-1', - table_name: 'TableName', - rows_ids: ['row-1'], - }, - }, - ); - }); - - it('write APIs send preview_table_config', () => { - const { api, post, put, del } = createApi(); - const previewTableConfig = { table_id: 'tbl-1', permissions: {} }; - - api.addRow('ai_agent', 'TableName', { Name: 'A' }, previewTableConfig); - api.addRows('ai_agent', 'TableName', [{ Name: 'B' }], previewTableConfig); - api.updateRow('ai_agent', 'TableName', 'row-1', { Name: 'C' }, previewTableConfig); - api.updateRows('ai_agent', 'TableName', [{ row_id: 'row-1', row: { Name: 'D' } }], previewTableConfig); - api.deleteRows('ai_agent', 'TableName', ['row-1'], previewTableConfig); - - expect(post).toHaveBeenNthCalledWith(1, expect.any(String), { - page_id: 'ai_agent', - table_name: 'TableName', - row_data: { Name: 'A' }, - preview_table_config: previewTableConfig, - }); - expect(post).toHaveBeenNthCalledWith(2, expect.any(String), { - page_id: 'ai_agent', - table_name: 'TableName', - rows_data: [{ Name: 'B' }], - preview_table_config: previewTableConfig, - }); - expect(put).toHaveBeenNthCalledWith(1, expect.any(String), { - page_id: 'ai_agent', - table_name: 'TableName', - row_id: 'row-1', - row_data: { Name: 'C' }, - preview_table_config: previewTableConfig, - }, expect.any(Object)); - expect(put).toHaveBeenNthCalledWith(2, expect.any(String), { - page_id: 'ai_agent', - table_name: 'TableName', - rows_data: [{ row_id: 'row-1', row: { Name: 'D' } }], - preview_table_config: previewTableConfig, - }, expect.any(Object)); - expect(del).toHaveBeenCalledWith(expect.any(String), { - headers: { 'Content-Type': 'application/json' }, - data: { - page_id: 'ai_agent', - table_name: 'TableName', - rows_ids: ['row-1'], - preview_table_config: previewTableConfig, - }, - }); - }); -}); - -describe('HTMLPageAPI link operations', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('adds a link and returns the updated row', () => { - const { api, post } = createApi(); - const updatedRow = { - _id: 'task-row-1', - 'Related projects': [ - { row_id: 'project-row-1', display_value: 'Project 1' }, - ], - }; - const response = { data: { success: true, row: updatedRow } }; - post.mockReturnValue(response); - - const result = api.addLink( - 'page-1', - 'Tasks', - 'task-row-1', - 'Related projects', - 'project-row-1', - ); - - expect(result).toBe(response); - expect(result.data.row).toEqual(updatedRow); - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/', - { - page_id: 'page-1', - table_name: 'Tasks', - row_id: 'task-row-1', - link_column_name: 'Related projects', - other_row_id: 'project-row-1', - }, - ); - }); - - it('deletes a link and returns the updated row', () => { - const { api, del } = createApi(); - const updatedRow = { _id: 'task-row-1', 'Related projects': [] }; - const response = { data: { success: true, row: updatedRow } }; - del.mockReturnValue(response); - - const result = api.deleteLink( - 'page-1', - 'Tasks', - 'task-row-1', - 'Related projects', - 'project-row-1', - ); - - expect(result).toBe(response); - expect(result.data.row).toEqual(updatedRow); - expect(del).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/', - { - headers: { 'Content-Type': 'application/json' }, - data: { - page_id: 'page-1', - table_name: 'Tasks', - row_id: 'task-row-1', - link_column_name: 'Related projects', - other_row_id: 'project-row-1', - }, - }, - ); - }); - - it('adds and deletes links in batch and returns the updated rows', () => { - const { api, post, del } = createApi(); - const linksData = [ - { - row_id: 'task-row-1', - links: { - 'Related projects': ['project-row-1', 'project-row-2'], - }, - }, - ]; - const addedRows = [{ - _id: 'task-row-1', - 'Related projects': [ - { row_id: 'project-row-1', display_value: 'Project 1' }, - { row_id: 'project-row-2', display_value: 'Project 2' }, - ], - }]; - const deletedRows = [{ _id: 'task-row-1', 'Related projects': [] }]; - const addResponse = { data: { success: true, rows: addedRows } }; - const deleteResponse = { data: { success: true, rows: deletedRows } }; - post.mockReturnValue(addResponse); - del.mockReturnValue(deleteResponse); - - const addResult = api.addLinks('page-1', 'Tasks', linksData); - const deleteResult = api.deleteLinks('page-1', 'Tasks', linksData); - - expect(addResult).toBe(addResponse); - expect(addResult.data.rows).toEqual(addedRows); - expect(deleteResult).toBe(deleteResponse); - expect(deleteResult.data.rows).toEqual(deletedRows); - expect(post).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/batch/', - { - page_id: 'page-1', - table_name: 'Tasks', - links_data: linksData, - }, - ); - expect(del).toHaveBeenCalledWith( - 'https://example.com/api/v2.1/universal-apps/app-uuid/html-page-links/batch/', - { - headers: { 'Content-Type': 'application/json' }, - data: { - page_id: 'page-1', - table_name: 'Tasks', - links_data: linksData, - }, - }, - ); - }); - - it('includes preview_table_config in link requests for ai_agent preview', () => { - const { api, post, del } = createApi(); - const previewTableConfig = { table_id: 'table-1', permissions: {} }; - const linksData = [{ row_id: 'task-row-1', links: { related: ['project-row-1'] } }]; - - api.addLink('ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', previewTableConfig); - api.deleteLink('ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', previewTableConfig); - api.addLinks('ai_agent', 'Tasks', linksData, previewTableConfig); - api.deleteLinks('ai_agent', 'Tasks', linksData, previewTableConfig); - - expect(post).toHaveBeenNthCalledWith(1, expect.any(String), { - page_id: 'ai_agent', - table_name: 'Tasks', - row_id: 'task-row-1', - link_column_name: 'related', - other_row_id: 'project-row-1', - preview_table_config: previewTableConfig, - }); - expect(del).toHaveBeenNthCalledWith(1, expect.any(String), { - headers: { 'Content-Type': 'application/json' }, - data: { - page_id: 'ai_agent', - table_name: 'Tasks', - row_id: 'task-row-1', - link_column_name: 'related', - other_row_id: 'project-row-1', - preview_table_config: previewTableConfig, - }, - }); - expect(post).toHaveBeenNthCalledWith(2, expect.any(String), { - page_id: 'ai_agent', - table_name: 'Tasks', - links_data: linksData, - preview_table_config: previewTableConfig, - }); - expect(del).toHaveBeenNthCalledWith(2, expect.any(String), { - headers: { 'Content-Type': 'application/json' }, - data: { - page_id: 'ai_agent', - table_name: 'Tasks', - links_data: linksData, - preview_table_config: previewTableConfig, - }, - }); - }); -}); - -describe('HTMLPageAPI.upload', () => { - beforeEach(() => { - jest.clearAllMocks(); - global.FormData = class { - constructor() { - this.fields = []; - } - - append(key, value, filename) { - this.fields.push([key, value, filename]); - } - }; - }); - - it('gets an upload link and uploads a file', async () => { - const get = jest.fn(); - axios.create.mockReturnValue({ get }); - - const api = new HTMLPageAPI(); - api.init({ - server: 'https://example.com/', - accessToken: 'token', - appUuid: 'app-uuid', - }); - - api.getUploadLink = jest.fn().mockResolvedValue({ - data: { - upload_link: 'https://upload.example.com/upload', - parent_path: '/parent', - file_relative_path: 'files', - img_relative_path: 'images', - asset_parent_url: 'https://assets.example.com', - }, - }); - api.uploadAsset = jest.fn().mockResolvedValue({ - data: [{ name: 'file.txt', size: 12 }], - }); - - const file = { name: 'file.txt' }; - const result = await api.upload('page-1', file); - - expect(api.getUploadLink).toHaveBeenCalledWith('page-1', 'file'); - expect(api.uploadAsset).toHaveBeenCalledWith( - 'https://upload.example.com/upload', - expect.any(FormData), - ); - expect(result).toEqual({ - name: 'file.txt', - size: 12, - type: 'file', - url: 'https://assets.example.com/files/file.txt', - }); - }); - - it('uses image relative path when uploading an image', async () => { - const get = jest.fn(); - axios.create.mockReturnValue({ get }); - - const api = new HTMLPageAPI(); - api.init({ - server: 'https://example.com/', - accessToken: 'token', - appUuid: 'app-uuid', - }); - - api.getUploadLink = jest.fn().mockResolvedValue({ - data: { - upload_link: 'https://upload.example.com/upload', - parent_path: '/parent', - file_relative_path: 'files', - img_relative_path: 'images', - asset_parent_url: 'https://assets.example.com', - }, - }); - api.uploadAsset = jest.fn().mockResolvedValue({ - data: [{ name: 'image.png', size: 34 }], - }); - - const file = { name: 'image.png' }; - const result = await api.upload('page-1', file, 'image'); - - expect(api.getUploadLink).toHaveBeenCalledWith('page-1', 'image'); - expect(result).toEqual({ - name: 'image.png', - size: 34, - type: 'image', - url: 'https://assets.example.com/images/image.png', - }); - }); -}); - -describe('HTMLPageAPI.initWithAccountToken', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('rejects with the access-token request error', async () => { - axios.get.mockRejectedValue(new Error('Request failed with status code 401')); - const api = new HTMLPageAPI(); - - await expect(api.initWithAccountToken({ - server: 'https://custom-app-server.example.com/', - accountToken: 'invalid-account-token', - appUuid: 'app-uuid', - })).rejects.toThrow('Failed to get access token: Request failed with status code 401'); - }); - - it('requires development access-token configuration', async () => { - const api = new HTMLPageAPI(); - - await expect(api.initWithAccountToken({ - server: 'https://custom-app-server.example.com/', - accountToken: 'account-token', - appUuid: '', - })).rejects.toThrow('Failed to get access token: missing server, accountToken, or appUuid'); - expect(axios.get).not.toHaveBeenCalled(); - }); - - it('rejects when the access-token response does not contain an access token', async () => { - axios.get.mockResolvedValue({ data: {} }); - const api = new HTMLPageAPI(); - - const error = await api.initWithAccountToken({ - server: 'https://custom-app-server.example.com/', - accountToken: 'account-token', - appUuid: 'app-uuid', - }).catch(error => error); - - expect(error.message).toBe('Failed to get access token: access_token missing'); - }); -}); - -describe('HTMLPageAPI.getParentOrigin', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('uses the custom-app-server bootstrap endpoint with the app access token', async () => { - axios.post.mockResolvedValue({ data: { parentOrigin: 'https://app.example.com' } }); - const api = new HTMLPageAPI(); - - await expect(api.getParentOrigin({ - server: 'https://custom-app-server.example.com/', - accessToken: 'access-token', - appUuid: 'app-uuid', - })).resolves.toBe('https://app.example.com'); - - expect(axios.post).toHaveBeenCalledWith( - 'https://custom-app-server.example.com/api/v2.1/universal-apps/bootstrap/', - { app_uuid: 'app-uuid' }, - { headers: { Authorization: 'Token access-token' } }, - ); - }); - - it('adds parentOrigin context when the bootstrap request fails', async () => { - axios.post.mockRejectedValue(new Error('Request failed with status code 401')); - const api = new HTMLPageAPI(); - - await expect(api.getParentOrigin({ - server: 'https://custom-app-server.example.com/', - accessToken: 'invalid-access-token', - appUuid: 'app-uuid', - })).rejects.toThrow('Failed to get parentOrigin: Request failed with status code 401'); - }); - - it('requires bootstrap configuration', async () => { - const api = new HTMLPageAPI(); - - await expect(api.getParentOrigin({ - server: 'https://custom-app-server.example.com/', - accessToken: 'access-token', - appUuid: '', - })).rejects.toThrow('Failed to get parentOrigin: missing server, accessToken, or appUuid'); - expect(axios.post).not.toHaveBeenCalled(); - }); -}); diff --git a/tests/sdk-action.test.js b/tests/sdk-action.test.js new file mode 100644 index 0000000..ebe7a26 --- /dev/null +++ b/tests/sdk-action.test.js @@ -0,0 +1,175 @@ +import { HTMLPageSDK } from '../src/sdk'; + +const mockSendNotification = jest.fn(); +const mockSendEmail = jest.fn(); +const mockGetMessageStatus = jest.fn(); +const mockRunScript = jest.fn(); +const mockGetScriptResult = jest.fn(); + +jest.mock('../src/iframe-adapter', () => ({ + IframeAdapter: jest.fn().mockImplementation(() => ({ + request: jest.fn(), + })), + POST_MESSAGE_REQUEST_TYPE: { + GET_SERVER: 'get_server', + GET_ACCESS_TOKEN: 'get_access_token', + GET_APP_UUID: 'get_app_uuid', + GET_PAGE_ID: 'get_page_id', + GET_PREVIEW_TABLE_CONFIGS: 'get_preview_table_configs', + }, +})); + +jest.mock('../src/apis/html-page-api', () => { + return jest.fn().mockImplementation(() => ({ + sendNotification: mockSendNotification, + sendEmail: mockSendEmail, + getMessageStatus: mockGetMessageStatus, + runScript: mockRunScript, + getScriptResult: mockGetScriptResult, + })); +}); + +describe('action operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('delegates notification and email requests with public parameter names', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { + sendNotification: mockSendNotification, + sendEmail: mockSendEmail, + }; + const notificationResponse = { data: { task_id: 'notification-task-1' } }; + const emailResponse = { data: { task_id: 'email-task-1' } }; + mockSendNotification.mockReturnValue(notificationResponse); + mockSendEmail.mockReturnValue(emailResponse); + + const notificationResult = sdk.sendNotification({ + tableName: 'Products', + rowId: 'row-1', + emails: ['recipient@example.com'], + msg: 'hello {Name}', + }); + const emailResult = sdk.sendEmail({ + tableName: 'Products', + rowId: 'row-1', + accountName: 'Operations SMTP', + sendTo: ['recipient@example.com'], + copyTo: ['audit@example.com'], + replyTo: ['service@example.com'], + subject: 'Order {Order number}', + htmlMessage: 'Hello
', + attachmentColumnNames: ['Attachment'], + attachmentColumnKeys: ['attachment-key'], + }); + + expect(notificationResult).toBe(notificationResponse); + expect(emailResult).toBe(emailResponse); + expect(mockSendNotification).toHaveBeenCalledWith( + 'page-1', + { + table_name: 'Products', + row_id: 'row-1', + emails: ['recipient@example.com'], + msg: 'hello {Name}', + }, + undefined, + ); + expect(mockSendEmail).toHaveBeenCalledWith( + 'page-1', + { + table_name: 'Products', + row_id: 'row-1', + account_name: 'Operations SMTP', + send_to: ['recipient@example.com'], + copy_to: ['audit@example.com'], + reply_to: ['service@example.com'], + subject: 'Order {Order number}', + message: undefined, + html_message: 'Hello
', + attachment_column_names: ['Attachment'], + }, + undefined, + ); + expect(mockSendEmail.mock.calls[0][1]).not.toHaveProperty('attachment_column_keys'); + }); + + it('passes matching preview permissions for ai_agent table action requests', () => { + const previewTableConfig = { + table_id: 'table-1', + table_name: 'Products', + permissions: { + view_rows_permission: { enabled: true, columns_keys: ['name'] }, + }, + }; + const expectedConfig = { + table_id: 'table-1', + permissions: previewTableConfig.permissions, + }; + const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); + sdk.htmlPageAPI = { + sendNotification: mockSendNotification, + sendEmail: mockSendEmail, + runScript: mockRunScript, + }; + + sdk.sendNotification({ + tableName: 'Products', rowId: 'row-1', emails: ['recipient@example.com'], msg: 'hello', + }); + sdk.sendEmail({ + tableName: 'Products', rowId: 'row-1', accountName: 'SMTP', sendTo: ['recipient@example.com'], subject: 'Hello', message: 'Hello', + }); + sdk.runScript({ scriptName: 'Generate report', tableName: 'Products', rowId: 'row-1' }); + + expect(mockSendNotification).toHaveBeenCalledWith( + 'ai_agent', expect.any(Object), expectedConfig, + ); + expect(mockSendEmail).toHaveBeenCalledWith( + 'ai_agent', expect.any(Object), expectedConfig, + ); + expect(mockRunScript).toHaveBeenCalledWith( + 'Generate report', 'ai_agent', 'Products', 'row-1', expectedConfig, + ); + }); + + it('delegates message-status and script requests with task IDs and the current page ID', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { + getMessageStatus: mockGetMessageStatus, + runScript: mockRunScript, + getScriptResult: mockGetScriptResult, + }; + const statusResponse = { data: { status: 'SUCCESS' } }; + const runResponse = { data: { task_id: 42 } }; + const resultResponse = { + data: { + script: { + state: 'finished', + success: false, + output: 'Script execution failed', + return_code: 125, + }, + }, + }; + mockGetMessageStatus.mockReturnValue(statusResponse); + mockRunScript.mockReturnValue(runResponse); + mockGetScriptResult.mockReturnValue(resultResponse); + + expect(sdk.getMessageStatus({ taskId: 'message-task-1' })).toBe(statusResponse); + expect(sdk.runScript({ scriptName: 'Generate report' })).toBe(runResponse); + const scriptResult = sdk.getScriptResult({ scriptName: 'Generate report', taskId: 42 }); + expect(scriptResult).toBe(resultResponse); + expect(scriptResult.data.script).toEqual({ + state: 'finished', + success: false, + output: 'Script execution failed', + return_code: 125, + }); + + expect(mockGetMessageStatus).toHaveBeenCalledWith('message-task-1'); + expect(mockRunScript).toHaveBeenCalledWith('Generate report', 'page-1', undefined, undefined, undefined); + expect(mockGetScriptResult).toHaveBeenCalledWith('Generate report', 42, 'page-1'); + }); +}); + diff --git a/tests/sdk-links.test.js b/tests/sdk-links.test.js new file mode 100644 index 0000000..f9cb5e4 --- /dev/null +++ b/tests/sdk-links.test.js @@ -0,0 +1,160 @@ +import { HTMLPageSDK } from '../src/sdk'; + +const mockAddLink = jest.fn(); +const mockDeleteLink = jest.fn(); +const mockAddLinks = jest.fn(); +const mockDeleteLinks = jest.fn(); + +jest.mock('../src/iframe-adapter', () => ({ + IframeAdapter: jest.fn().mockImplementation(() => ({ + request: jest.fn(), + })), + POST_MESSAGE_REQUEST_TYPE: { + GET_SERVER: 'get_server', + GET_ACCESS_TOKEN: 'get_access_token', + GET_APP_UUID: 'get_app_uuid', + GET_PAGE_ID: 'get_page_id', + GET_PREVIEW_TABLE_CONFIGS: 'get_preview_table_configs', + }, +})); + +jest.mock('../src/apis/html-page-api', () => { + return jest.fn().mockImplementation(() => ({ + addLink: mockAddLink, + deleteLink: mockDeleteLink, + addLinks: mockAddLinks, + deleteLinks: mockDeleteLinks, + })); +}); + +describe('links', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('delegates single and batch link operations', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { + addLink: mockAddLink, + deleteLink: mockDeleteLink, + addLinks: mockAddLinks, + deleteLinks: mockDeleteLinks, + }; + const linksData = [ + { + row_id: 'task-row-1', + links: { 'Related projects': ['project-row-1', 'project-row-2'] }, + }, + ]; + const addedRow = { + _id: 'task-row-1', + 'Related projects': [{ row_id: 'project-row-1', display_value: 'Project 1' }], + }; + const deletedRow = { _id: 'task-row-1', 'Related projects': [] }; + const addedRows = [{ + _id: 'task-row-1', + 'Related projects': [ + { row_id: 'project-row-1', display_value: 'Project 1' }, + { row_id: 'project-row-2', display_value: 'Project 2' }, + ], + }]; + const deletedRows = [{ _id: 'task-row-1', 'Related projects': [] }]; + const addResponse = { data: { success: true, row: addedRow } }; + const deleteResponse = { data: { success: true, row: deletedRow } }; + const batchAddResponse = { data: { success: true, rows: addedRows } }; + const batchDeleteResponse = { data: { success: true, rows: deletedRows } }; + mockAddLink.mockReturnValue(addResponse); + mockDeleteLink.mockReturnValue(deleteResponse); + mockAddLinks.mockReturnValue(batchAddResponse); + mockDeleteLinks.mockReturnValue(batchDeleteResponse); + + const addResult = sdk.addLink({ + tableName: 'Tasks', + rowId: 'task-row-1', + linkColumnName: 'Related projects', + otherRowId: 'project-row-1', + }); + const deleteResult = sdk.deleteLink({ + tableName: 'Tasks', + rowId: 'task-row-1', + linkColumnName: 'Related projects', + otherRowId: 'project-row-1', + }); + const batchAddResult = sdk.batchAddLinks({ tableName: 'Tasks', linksData }); + const batchDeleteResult = sdk.batchDeleteLinks({ tableName: 'Tasks', linksData }); + + expect(addResult).toBe(addResponse); + expect(addResult.data.row).toEqual(addedRow); + expect(deleteResult).toBe(deleteResponse); + expect(deleteResult.data.row).toEqual(deletedRow); + expect(batchAddResult).toBe(batchAddResponse); + expect(batchAddResult.data.rows).toEqual(addedRows); + expect(batchDeleteResult).toBe(batchDeleteResponse); + expect(batchDeleteResult.data.rows).toEqual(deletedRows); + expect(mockAddLink).toHaveBeenCalledWith( + 'page-1', + 'Tasks', + 'task-row-1', + 'Related projects', + 'project-row-1', + undefined, + ); + expect(mockDeleteLink).toHaveBeenCalledWith( + 'page-1', + 'Tasks', + 'task-row-1', + 'Related projects', + 'project-row-1', + undefined, + ); + expect(mockAddLinks).toHaveBeenCalledWith('page-1', 'Tasks', linksData, undefined); + expect(mockDeleteLinks).toHaveBeenCalledWith('page-1', 'Tasks', linksData, undefined); + }); + + it('includes the matching table permissions for ai_agent preview', () => { + const previewTableConfig = { + table_id: 'table-1', + table_name: 'Tasks', + permissions: { + edit_rows_permission: { enabled: true, columns_keys: ['related'] }, + }, + }; + const expectedConfig = { + table_id: 'table-1', + permissions: previewTableConfig.permissions, + }; + const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); + sdk.htmlPageAPI = { + addLink: mockAddLink, + deleteLink: mockDeleteLink, + addLinks: mockAddLinks, + deleteLinks: mockDeleteLinks, + }; + const linksData = [{ row_id: 'task-row-1', links: { related: ['project-row-1'] } }]; + + sdk.addLink({ + tableName: 'Tasks', + rowId: 'task-row-1', + linkColumnName: 'related', + otherRowId: 'project-row-1', + }); + sdk.deleteLink({ + tableName: 'Tasks', + rowId: 'task-row-1', + linkColumnName: 'related', + otherRowId: 'project-row-1', + }); + sdk.batchAddLinks({ tableName: 'Tasks', linksData }); + sdk.batchDeleteLinks({ tableName: 'Tasks', linksData }); + + expect(mockAddLink).toHaveBeenCalledWith( + 'ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', expectedConfig, + ); + expect(mockDeleteLink).toHaveBeenCalledWith( + 'ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', expectedConfig, + ); + expect(mockAddLinks).toHaveBeenCalledWith('ai_agent', 'Tasks', linksData, expectedConfig); + expect(mockDeleteLinks).toHaveBeenCalledWith('ai_agent', 'Tasks', linksData, expectedConfig); + }); +}); + diff --git a/tests/sdk-rows.test.js b/tests/sdk-rows.test.js new file mode 100644 index 0000000..d845a87 --- /dev/null +++ b/tests/sdk-rows.test.js @@ -0,0 +1,250 @@ +import { HTMLPageSDK } from '../src/sdk'; + +const mockListRows = jest.fn(); +const mockQueryRows = jest.fn(); +const mockAddRow = jest.fn(); +const mockUpdateRow = jest.fn(); +const mockDeleteRows = jest.fn(); +const mockAddRows = jest.fn(); +const mockUpdateRows = jest.fn(); + +jest.mock('../src/iframe-adapter', () => ({ + IframeAdapter: jest.fn().mockImplementation(() => ({ + request: jest.fn(), + })), + POST_MESSAGE_REQUEST_TYPE: { + GET_SERVER: 'get_server', + GET_ACCESS_TOKEN: 'get_access_token', + GET_APP_UUID: 'get_app_uuid', + GET_PAGE_ID: 'get_page_id', + GET_PREVIEW_TABLE_CONFIGS: 'get_preview_table_configs', + }, +})); + +jest.mock('../src/apis/html-page-api', () => { + return jest.fn().mockImplementation(() => ({ + listRows: mockListRows, + queryRows: mockQueryRows, + addRow: mockAddRow, + updateRow: mockUpdateRow, + deleteRows: mockDeleteRows, + addRows: mockAddRows, + updateRows: mockUpdateRows, + })); +}); + +describe('rows', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('list rows', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { listRows: mockListRows }; + + const response = { data: { rows: [{ _id: 'row-1', '0000': 'John' }] } }; + mockListRows.mockReturnValue(response); + + const result = sdk.listRows({ + tableName: 'TableName', + start: 0, + limit: 100, + }); + + expect(result).toBe(response); + expect(mockListRows).toHaveBeenCalledWith('page-1', 'TableName', 0, 100, undefined); + }); + + it('query rows by table and column names', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { queryRows: mockQueryRows }; + + const conditions = [{ columnName: 'Order Number', value: 'TEST2026070001' }]; + sdk.queryRows({ tableName: 'Order', conditions }); + + expect(mockQueryRows).toHaveBeenCalledWith('page-1', 'Order', conditions, undefined, undefined, undefined); + }); + + it('query rows includes the configured table permissions for ai_agent preview', () => { + const previewTableConfig = { + table_id: 'REW7', + table_name: 'Order', + permissions: { + query_rows_permission: { + enabled: true, + columns_keys: ['0000', 'wbyd'], + query_columns: [{ column_key: '0000', enable_fuzzy_query: true, case_sensitive: false }], + }, + }, + }; + const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); + sdk.htmlPageAPI = { queryRows: mockQueryRows }; + + const conditions = [{ columnName: 'Order Number', value: '202607' }]; + sdk.queryRows({ tableName: 'Order', conditions }); + + expect(mockQueryRows).toHaveBeenCalledWith( + 'ai_agent', + 'Order', + conditions, + undefined, + undefined, + { + table_id: 'REW7', + permissions: previewTableConfig.permissions, + }, + ); + }); + + it('row APIs include the matching table permissions for ai_agent preview', () => { + const previewTableConfig = { + table_id: 'REW7', + table_name: 'Order', + permissions: { + view_rows_permission: { enabled: true, columns_keys: ['0000'] }, + add_rows_permission: { enabled: true, columns_keys: ['0000'] }, + edit_rows_permission: { enabled: true, columns_keys: ['0000'] }, + delete_rows_permission: { enabled: true }, + }, + }; + const expectedConfig = { + table_id: 'REW7', + permissions: previewTableConfig.permissions, + }; + const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); + sdk.htmlPageAPI = { + listRows: mockListRows, + addRow: mockAddRow, + updateRow: mockUpdateRow, + deleteRows: mockDeleteRows, + addRows: mockAddRows, + updateRows: mockUpdateRows, + }; + + sdk.listRows({ tableName: 'Order' }); + sdk.addRow({ tableName: 'Order', rowData: { OrderNumber: '001' } }); + sdk.updateRow({ tableName: 'Order', rowId: 'row-1', rowData: { OrderNumber: '002' } }); + sdk.batchAddRows({ tableName: 'Order', rowsData: [{ OrderNumber: '003' }] }); + sdk.batchUpdateRows({ tableName: 'Order', rowsData: [{ row_id: 'row-1', row: { OrderNumber: '004' } }] }); + sdk.batchDeleteRows({ tableName: 'Order', rowsIds: ['row-1'] }); + + expect(mockListRows).toHaveBeenCalledWith('ai_agent', 'Order', undefined, undefined, expectedConfig); + expect(mockAddRow).toHaveBeenCalledWith('ai_agent', 'Order', { OrderNumber: '001' }, expectedConfig); + expect(mockUpdateRow).toHaveBeenCalledWith('ai_agent', 'Order', 'row-1', { OrderNumber: '002' }, expectedConfig); + expect(mockAddRows).toHaveBeenCalledWith('ai_agent', 'Order', [{ OrderNumber: '003' }], expectedConfig); + expect(mockUpdateRows).toHaveBeenCalledWith( + 'ai_agent', + 'Order', + [{ row_id: 'row-1', row: { OrderNumber: '004' } }], + expectedConfig, + ); + expect(mockDeleteRows).toHaveBeenCalledWith('ai_agent', 'Order', ['row-1'], expectedConfig); + }); + + it('add/batchAdd/update/batchUpdate/delete/batchDelete rows', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { + addRow: mockAddRow, + updateRow: mockUpdateRow, + deleteRows: mockDeleteRows, + addRows: mockAddRows, + updateRows: mockUpdateRows, + }; + + const addRowResponse = { data: { success: true, row: { _id: 'row-1', '0000': 'John' } } }; + const updateRowResponse = { data: { success: true, row: { _id: 'row-1', '0000': 'Jane' } } }; + const deleteRowsResponse = { data: { success: true } }; + const addRowsResponse = { + data: { success: true }, + rows: [{ _id: 'row-1', '0000': 'John' }], + inserted_row_count: 1, + }; + const updateRowsResponse = { + data: { + success: true, + rows: [{ _id: 'row-1', '0000': 'Jane' }], + }, + }; + + mockAddRow.mockReturnValue(addRowResponse); + mockUpdateRow.mockReturnValue(updateRowResponse); + mockDeleteRows.mockReturnValue(deleteRowsResponse); + mockAddRows.mockReturnValue(addRowsResponse); + mockUpdateRows.mockReturnValue(updateRowsResponse); + + expect(sdk.addRow({ tableName: 'TableName', rowData: { Name: 'John' } })).toEqual(addRowResponse); + expect(sdk.updateRow({ tableName: 'TableName', rowId: 'row-1', rowData: { Name: 'Jane' } })).toEqual(updateRowResponse); + expect(sdk.deleteRow({ tableName: 'TableName', rowId: 'row-1' })).toEqual(deleteRowsResponse); + expect(sdk.batchAddRows({ tableName: 'TableName', rowsData: [{ Name: 'John' }] })).toEqual(addRowsResponse); + expect(sdk.batchUpdateRows({ + tableName: 'TableName', + rowsData: [{ row_id: 'row-1', row: { Name: 'Jane' } }], + })).toEqual(updateRowsResponse); + expect(sdk.batchDeleteRows({ tableName: 'TableName', rowsIds: ['row-1'] })).toEqual(deleteRowsResponse); + + expect(mockAddRow).toHaveBeenCalledWith('page-1', 'TableName', { Name: 'John' }, undefined); + expect(mockUpdateRow).toHaveBeenCalledWith('page-1', 'TableName', 'row-1', { Name: 'Jane' }, undefined); + expect(mockDeleteRows).toHaveBeenCalledWith('page-1', 'TableName', ['row-1'], undefined); + expect(mockAddRows).toHaveBeenCalledWith('page-1', 'TableName', [{ Name: 'John' }], undefined); + expect(mockUpdateRows).toHaveBeenCalledWith( + 'page-1', + 'TableName', + [{ row_id: 'row-1', row: { Name: 'Jane' } }], + undefined, + ); + expect(mockDeleteRows).toHaveBeenCalledTimes(2); + }); + + it('updateRow and batchUpdateRows support link-column replacements', () => { + const sdk = new HTMLPageSDK({ pageId: 'page-1' }); + sdk.htmlPageAPI = { + updateRow: mockUpdateRow, + updateRows: mockUpdateRows, + }; + const rowData = { + Name: 'Updated task', + 'Related projects': ['project-row-1', 'project-row-2'], + }; + const rowsData = [ + { + row_id: 'task-row-1', + row: rowData, + }, + { + row_id: 'task-row-2', + row: { 'Related projects': [] }, + }, + ]; + const updatedRow = { + _id: 'task-row-1', + Name: 'Updated task', + 'Related projects': [ + { row_id: 'project-row-1', display_value: 'Project 1' }, + { row_id: 'project-row-2', display_value: 'Project 2' }, + ], + }; + const updatedRows = [ + updatedRow, + { _id: 'task-row-2', 'Related projects': [] }, + ]; + const updateResponse = { data: { success: true, row: updatedRow } }; + const batchUpdateResponse = { data: { success: true, rows: updatedRows } }; + mockUpdateRow.mockReturnValue(updateResponse); + mockUpdateRows.mockReturnValue(batchUpdateResponse); + + const result = sdk.updateRow({ tableName: 'Tasks', rowId: 'task-row-1', rowData }); + const batchResult = sdk.batchUpdateRows({ tableName: 'Tasks', rowsData }); + + expect(result.data.row).toEqual(updatedRow); + expect(batchResult.data.rows).toEqual(updatedRows); + expect(mockUpdateRow).toHaveBeenCalledWith( + 'page-1', + 'Tasks', + 'task-row-1', + rowData, + undefined, + ); + expect(mockUpdateRows).toHaveBeenCalledWith('page-1', 'Tasks', rowsData, undefined); + }); +}); + diff --git a/tests/sdk.test.js b/tests/sdk.test.js index ed84328..cdf9abc 100644 --- a/tests/sdk.test.js +++ b/tests/sdk.test.js @@ -1,20 +1,9 @@ import { HTMLPageSDK } from '../src/sdk'; -const mockListRows = jest.fn(); -const mockQueryRows = jest.fn(); +const mockUpload = jest.fn(); const mockListCollaborators = jest.fn(); const mockGetCurrentUser = jest.fn(); const mockResolveUsers = jest.fn(); -const mockAddRow = jest.fn(); -const mockUpdateRow = jest.fn(); -const mockDeleteRows = jest.fn(); -const mockAddRows = jest.fn(); -const mockUpdateRows = jest.fn(); -const mockAddLink = jest.fn(); -const mockDeleteLink = jest.fn(); -const mockAddLinks = jest.fn(); -const mockDeleteLinks = jest.fn(); -const mockUpload = jest.fn(); jest.mock('../src/iframe-adapter', () => ({ IframeAdapter: jest.fn().mockImplementation(() => ({ @@ -31,131 +20,18 @@ jest.mock('../src/iframe-adapter', () => ({ jest.mock('../src/apis/html-page-api', () => { return jest.fn().mockImplementation(() => ({ - listRows: mockListRows, + upload: mockUpload, listCollaborators: mockListCollaborators, getCurrentUser: mockGetCurrentUser, resolveUsers: mockResolveUsers, - addRow: mockAddRow, - updateRow: mockUpdateRow, - deleteRows: mockDeleteRows, - addRows: mockAddRows, - updateRows: mockUpdateRows, - addLink: mockAddLink, - deleteLink: mockDeleteLink, - addLinks: mockAddLinks, - deleteLinks: mockDeleteLinks, - upload: mockUpload, })); }); -describe('rows', () => { +describe('collaborators and users', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('list rows', () => { - const sdk = new HTMLPageSDK({ pageId: 'page-1' }); - sdk.htmlPageAPI = { listRows: mockListRows }; - - const response = { data: { rows: [{ _id: 'row-1', '0000': 'John' }] } }; - mockListRows.mockReturnValue(response); - - const result = sdk.listRows({ - tableName: 'TableName', - start: 0, - limit: 100, - }); - - expect(result).toBe(response); - expect(mockListRows).toHaveBeenCalledWith('page-1', 'TableName', 0, 100, undefined); - }); - - it('query rows by table and column names', () => { - const sdk = new HTMLPageSDK({ pageId: 'page-1' }); - sdk.htmlPageAPI = { queryRows: mockQueryRows }; - - const conditions = [{ columnName: 'Order Number', value: 'TEST2026070001' }]; - sdk.queryRows({ tableName: 'Order', conditions }); - - expect(mockQueryRows).toHaveBeenCalledWith('page-1', 'Order', conditions, undefined, undefined, undefined); - }); - - it('query rows includes the configured table permissions for ai_agent preview', () => { - const previewTableConfig = { - table_id: 'REW7', - table_name: 'Order', - permissions: { - query_rows_permission: { - enabled: true, - columns_keys: ['0000', 'wbyd'], - query_columns: [{ column_key: '0000', enable_fuzzy_query: true, case_sensitive: false }], - }, - }, - }; - const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); - sdk.htmlPageAPI = { queryRows: mockQueryRows }; - - const conditions = [{ columnName: 'Order Number', value: '202607' }]; - sdk.queryRows({ tableName: 'Order', conditions }); - - expect(mockQueryRows).toHaveBeenCalledWith( - 'ai_agent', - 'Order', - conditions, - undefined, - undefined, - { - table_id: 'REW7', - permissions: previewTableConfig.permissions, - }, - ); - }); - - it('row APIs include the matching table permissions for ai_agent preview', () => { - const previewTableConfig = { - table_id: 'REW7', - table_name: 'Order', - permissions: { - view_rows_permission: { enabled: true, columns_keys: ['0000'] }, - add_rows_permission: { enabled: true, columns_keys: ['0000'] }, - edit_rows_permission: { enabled: true, columns_keys: ['0000'] }, - delete_rows_permission: { enabled: true }, - }, - }; - const expectedConfig = { - table_id: 'REW7', - permissions: previewTableConfig.permissions, - }; - const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); - sdk.htmlPageAPI = { - listRows: mockListRows, - addRow: mockAddRow, - updateRow: mockUpdateRow, - deleteRows: mockDeleteRows, - addRows: mockAddRows, - updateRows: mockUpdateRows, - }; - - sdk.listRows({ tableName: 'Order' }); - sdk.addRow({ tableName: 'Order', rowData: { OrderNumber: '001' } }); - sdk.updateRow({ tableName: 'Order', rowId: 'row-1', rowData: { OrderNumber: '002' } }); - sdk.batchAddRows({ tableName: 'Order', rowsData: [{ OrderNumber: '003' }] }); - sdk.batchUpdateRows({ tableName: 'Order', rowsData: [{ row_id: 'row-1', row: { OrderNumber: '004' } }] }); - sdk.batchDeleteRows({ tableName: 'Order', rowsIds: ['row-1'] }); - - expect(mockListRows).toHaveBeenCalledWith('ai_agent', 'Order', undefined, undefined, expectedConfig); - expect(mockAddRow).toHaveBeenCalledWith('ai_agent', 'Order', { OrderNumber: '001' }, expectedConfig); - expect(mockUpdateRow).toHaveBeenCalledWith('ai_agent', 'Order', 'row-1', { OrderNumber: '002' }, expectedConfig); - expect(mockAddRows).toHaveBeenCalledWith('ai_agent', 'Order', [{ OrderNumber: '003' }], expectedConfig); - expect(mockUpdateRows).toHaveBeenCalledWith( - 'ai_agent', - 'Order', - [{ row_id: 'row-1', row: { OrderNumber: '004' } }], - expectedConfig, - ); - expect(mockDeleteRows).toHaveBeenCalledWith('ai_agent', 'Order', ['row-1'], expectedConfig); - }); - it('list collaborators', () => { const sdk = new HTMLPageSDK({ pageId: 'page-1' }); sdk.htmlPageAPI = { listCollaborators: mockListCollaborators }; @@ -203,243 +79,6 @@ describe('rows', () => { expect(mockResolveUsers).toHaveBeenCalledWith(['user@example.com']); }); - it('add/batchAdd/update/batchUpdate/delete/batchDelete rows', () => { - const sdk = new HTMLPageSDK({ pageId: 'page-1' }); - sdk.htmlPageAPI = { - addRow: mockAddRow, - updateRow: mockUpdateRow, - deleteRows: mockDeleteRows, - addRows: mockAddRows, - updateRows: mockUpdateRows, - upload: mockUpload, - }; - - const addRowResponse = { data: { success: true, row: { _id: 'row-1', '0000': 'John' } } }; - const updateRowResponse = { data: { success: true, row: { _id: 'row-1', '0000': 'Jane' } } }; - const deleteRowsResponse = { data: { success: true } }; - const addRowsResponse = { - data: { success: true }, - rows: [{ _id: 'row-1', '0000': 'John' }], - inserted_row_count: 1, - }; - const updateRowsResponse = { - data: { - success: true, - rows: [{ _id: 'row-1', '0000': 'Jane' }], - }, - }; - - mockAddRow.mockReturnValue(addRowResponse); - mockUpdateRow.mockReturnValue(updateRowResponse); - mockDeleteRows.mockReturnValue(deleteRowsResponse); - mockAddRows.mockReturnValue(addRowsResponse); - mockUpdateRows.mockReturnValue(updateRowsResponse); - - expect(sdk.addRow({ tableName: 'TableName', rowData: { Name: 'John' } })).toEqual(addRowResponse); - expect(sdk.updateRow({ tableName: 'TableName', rowId: 'row-1', rowData: { Name: 'Jane' } })).toEqual(updateRowResponse); - expect(sdk.deleteRow({ tableName: 'TableName', rowId: 'row-1' })).toEqual(deleteRowsResponse); - expect(sdk.batchAddRows({ tableName: 'TableName', rowsData: [{ Name: 'John' }] })).toEqual(addRowsResponse); - expect(sdk.batchUpdateRows({ - tableName: 'TableName', - rowsData: [{ row_id: 'row-1', row: { Name: 'Jane' } }], - })).toEqual(updateRowsResponse); - expect(sdk.batchDeleteRows({ tableName: 'TableName', rowsIds: ['row-1'] })).toEqual(deleteRowsResponse); - - expect(mockAddRow).toHaveBeenCalledWith('page-1', 'TableName', { Name: 'John' }, undefined); - expect(mockUpdateRow).toHaveBeenCalledWith('page-1', 'TableName', 'row-1', { Name: 'Jane' }, undefined); - expect(mockDeleteRows).toHaveBeenCalledWith('page-1', 'TableName', ['row-1'], undefined); - expect(mockAddRows).toHaveBeenCalledWith('page-1', 'TableName', [{ Name: 'John' }], undefined); - expect(mockUpdateRows).toHaveBeenCalledWith( - 'page-1', - 'TableName', - [{ row_id: 'row-1', row: { Name: 'Jane' } }], - undefined, - ); - expect(mockDeleteRows).toHaveBeenCalledTimes(2); - }); - - it('updateRow and batchUpdateRows support link-column replacements', () => { - const sdk = new HTMLPageSDK({ pageId: 'page-1' }); - sdk.htmlPageAPI = { - updateRow: mockUpdateRow, - updateRows: mockUpdateRows, - }; - const rowData = { - Name: 'Updated task', - 'Related projects': ['project-row-1', 'project-row-2'], - }; - const rowsData = [ - { - row_id: 'task-row-1', - row: rowData, - }, - { - row_id: 'task-row-2', - row: { 'Related projects': [] }, - }, - ]; - const updatedRow = { - _id: 'task-row-1', - Name: 'Updated task', - 'Related projects': [ - { row_id: 'project-row-1', display_value: 'Project 1' }, - { row_id: 'project-row-2', display_value: 'Project 2' }, - ], - }; - const updatedRows = [ - updatedRow, - { _id: 'task-row-2', 'Related projects': [] }, - ]; - const updateResponse = { data: { success: true, row: updatedRow } }; - const batchUpdateResponse = { data: { success: true, rows: updatedRows } }; - mockUpdateRow.mockReturnValue(updateResponse); - mockUpdateRows.mockReturnValue(batchUpdateResponse); - - const result = sdk.updateRow({ tableName: 'Tasks', rowId: 'task-row-1', rowData }); - const batchResult = sdk.batchUpdateRows({ tableName: 'Tasks', rowsData }); - - expect(result.data.row).toEqual(updatedRow); - expect(batchResult.data.rows).toEqual(updatedRows); - expect(mockUpdateRow).toHaveBeenCalledWith( - 'page-1', - 'Tasks', - 'task-row-1', - rowData, - undefined, - ); - expect(mockUpdateRows).toHaveBeenCalledWith('page-1', 'Tasks', rowsData, undefined); - }); -}); - -describe('links', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('delegates single and batch link operations', () => { - const sdk = new HTMLPageSDK({ pageId: 'page-1' }); - sdk.htmlPageAPI = { - addLink: mockAddLink, - deleteLink: mockDeleteLink, - addLinks: mockAddLinks, - deleteLinks: mockDeleteLinks, - }; - const linksData = [ - { - row_id: 'task-row-1', - links: { 'Related projects': ['project-row-1', 'project-row-2'] }, - }, - ]; - const addedRow = { - _id: 'task-row-1', - 'Related projects': [{ row_id: 'project-row-1', display_value: 'Project 1' }], - }; - const deletedRow = { _id: 'task-row-1', 'Related projects': [] }; - const addedRows = [{ - _id: 'task-row-1', - 'Related projects': [ - { row_id: 'project-row-1', display_value: 'Project 1' }, - { row_id: 'project-row-2', display_value: 'Project 2' }, - ], - }]; - const deletedRows = [{ _id: 'task-row-1', 'Related projects': [] }]; - const addResponse = { data: { success: true, row: addedRow } }; - const deleteResponse = { data: { success: true, row: deletedRow } }; - const batchAddResponse = { data: { success: true, rows: addedRows } }; - const batchDeleteResponse = { data: { success: true, rows: deletedRows } }; - mockAddLink.mockReturnValue(addResponse); - mockDeleteLink.mockReturnValue(deleteResponse); - mockAddLinks.mockReturnValue(batchAddResponse); - mockDeleteLinks.mockReturnValue(batchDeleteResponse); - - const addResult = sdk.addLink({ - tableName: 'Tasks', - rowId: 'task-row-1', - linkColumnName: 'Related projects', - otherRowId: 'project-row-1', - }); - const deleteResult = sdk.deleteLink({ - tableName: 'Tasks', - rowId: 'task-row-1', - linkColumnName: 'Related projects', - otherRowId: 'project-row-1', - }); - const batchAddResult = sdk.batchAddLinks({ tableName: 'Tasks', linksData }); - const batchDeleteResult = sdk.batchDeleteLinks({ tableName: 'Tasks', linksData }); - - expect(addResult).toBe(addResponse); - expect(addResult.data.row).toEqual(addedRow); - expect(deleteResult).toBe(deleteResponse); - expect(deleteResult.data.row).toEqual(deletedRow); - expect(batchAddResult).toBe(batchAddResponse); - expect(batchAddResult.data.rows).toEqual(addedRows); - expect(batchDeleteResult).toBe(batchDeleteResponse); - expect(batchDeleteResult.data.rows).toEqual(deletedRows); - expect(mockAddLink).toHaveBeenCalledWith( - 'page-1', - 'Tasks', - 'task-row-1', - 'Related projects', - 'project-row-1', - undefined, - ); - expect(mockDeleteLink).toHaveBeenCalledWith( - 'page-1', - 'Tasks', - 'task-row-1', - 'Related projects', - 'project-row-1', - undefined, - ); - expect(mockAddLinks).toHaveBeenCalledWith('page-1', 'Tasks', linksData, undefined); - expect(mockDeleteLinks).toHaveBeenCalledWith('page-1', 'Tasks', linksData, undefined); - }); - - it('includes the matching table permissions for ai_agent preview', () => { - const previewTableConfig = { - table_id: 'table-1', - table_name: 'Tasks', - permissions: { - edit_rows_permission: { enabled: true, columns_keys: ['related'] }, - }, - }; - const expectedConfig = { - table_id: 'table-1', - permissions: previewTableConfig.permissions, - }; - const sdk = new HTMLPageSDK({ pageId: 'ai_agent', previewTableConfigs: [previewTableConfig] }); - sdk.htmlPageAPI = { - addLink: mockAddLink, - deleteLink: mockDeleteLink, - addLinks: mockAddLinks, - deleteLinks: mockDeleteLinks, - }; - const linksData = [{ row_id: 'task-row-1', links: { related: ['project-row-1'] } }]; - - sdk.addLink({ - tableName: 'Tasks', - rowId: 'task-row-1', - linkColumnName: 'related', - otherRowId: 'project-row-1', - }); - sdk.deleteLink({ - tableName: 'Tasks', - rowId: 'task-row-1', - linkColumnName: 'related', - otherRowId: 'project-row-1', - }); - sdk.batchAddLinks({ tableName: 'Tasks', linksData }); - sdk.batchDeleteLinks({ tableName: 'Tasks', linksData }); - - expect(mockAddLink).toHaveBeenCalledWith( - 'ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', expectedConfig, - ); - expect(mockDeleteLink).toHaveBeenCalledWith( - 'ai_agent', 'Tasks', 'task-row-1', 'related', 'project-row-1', expectedConfig, - ); - expect(mockAddLinks).toHaveBeenCalledWith('ai_agent', 'Tasks', linksData, expectedConfig); - expect(mockDeleteLinks).toHaveBeenCalledWith('ai_agent', 'Tasks', linksData, expectedConfig); - }); }); describe('upload', () => {