import { beforeEach, describe, destroy, expect, test } from "@odoo/hoot"; import { queryAllTexts, queryOne } from "@odoo/hoot-dom"; import { animationFrame, Deferred, mockTimeZone } from "@odoo/hoot-mock"; import { contains, defineModels, fields, findComponent, mockService, models, mountView, onRpc, patchWithCleanup, toggleMenuItem, toggleMenuItemOption, toggleSearchBarMenu, } from "@web/../tests/web_test_helpers"; import { session } from "@web/session"; import { MapController } from "@web_map/map_view/map_controller"; import { MapModel } from "@web_map/map_view/map_model"; import { MapRenderer } from "@web_map/map_view/map_renderer"; const MAP_BOX_TOKEN = "token"; function getMapController(view) { return findComponent(view, (c) => c instanceof MapController); } function getMapRenderer(view) { return findComponent(view, (c) => c instanceof MapRenderer); } const TEST_RECORDS = { task: { oneRecord: [{ id: 1, name: "Foo", partner_id: 1 }], twoRecordsFieldDateTime: [ { id: 1, name: "Foo", scheduled_date: false, partner_id: 1 }, { id: 2, name: "Bar", scheduled_date: "2022-02-07 21:09:31", partner_id: 2, }, ], twoRecords: [ { id: 1, name: "FooProject", sequence: 1, partner_id: 1 }, { id: 2, name: "BarProject", sequence: 2, partner_id: 2 }, ], threeRecords: [ { id: 1, name: "FooProject", sequence: 1, partner_id: 1, partner_ids: [1, 2], }, { id: 2, name: "BarProject", sequence: 2, partner_id: 2, partner_ids: [1], }, { id: 3, name: "FooBarProject", sequence: 3, partner_id: 1, partner_ids: [1], }, ], twoRecordOnePartner: [ { id: 1, name: "FooProject", partner_id: 1 }, { id: 2, name: "BarProject", partner_id: 1 }, ], recordWithouthPartner: [{ id: 1, name: "Foo", partner_id: false }], anotherPartnerId: [{ id: 1, name: "FooProject", another_partner_id: 1 }], }, partner: { coordinatesNoAddress: [ { id: 1, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, }, ], oneLocatedRecord: [ { id: 1, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 1, }, ], wrongCoordinatesNoAddress: [ { id: 1, name: "Foo", partner_latitude: 10000.0, partner_longitude: 100000.5, }, ], noCoordinatesGoodAddress: [ { id: 1, name: "Foo", partner_latitude: 0, partner_longitude: 0, contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", }, ], twoRecordsAddressNoCoordinates: [ { id: 2, name: "Foo", contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 3, }, { id: 1, name: "Bar", contact_address_complete: "Chaussée de Louvain 94, 5310 Éghezée", sequence: 1, }, ], twoRecordsAddressCoordinates: [ { id: 2, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 3, }, { id: 1, name: "Bar", partner_latitude: 10.0, partner_longitude: 10.5, contact_address_complete: "Chaussée de Louvain 94, 5310 Éghezée", sequence: 1, }, ], twoRecordsOneUnlocated: [ { id: 1, name: "Foo", contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 3, }, { id: 2, name: "Bar", }, ], threeRecords: [ { id: 1, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 1, user_id: 1, }, { id: 2, name: "Bar", partner_latitude: 11.0, partner_longitude: 11.5, contact_address_complete: "Chaussée de Wavre 50, 1367, Ramillies", sequence: 2, user_id: 2, }, { id: 3, name: "Baz", partner_latitude: 12.0, partner_longitude: 12.5, contact_address_complete: "Chaussée de Louvain 94, 5310 Éghezée", sequence: 3, user_id: false, }, ], unlocatedRecords: [{ id: 1, name: "Foo" }], noCoordinatesWrongAddress: [ { id: 1, name: "Foo", contact_address_complete: "Cfezfezfefes", }, ], }, }; class Task extends models.Model { _name = "project.task"; name = fields.Char(); scheduled_date = fields.Datetime({ string: "Schedule date" }); task_status = fields.Selection({ string: "Status", selection: [ ["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"], ], }); sequence = fields.Integer(); partner_id = fields.Many2one({ string: "partner", relation: "res.partner", }); another_partner_id = fields.Many2one({ string: "another relation", relation: "res.partner", }); partner_ids = fields.One2many({ string: "Partners", comodel_name: "res.partner", relation: "res.partner", relation_field: "task_id", }); _records = [{ id: 1, name: "project", partner_id: 1 }]; } class Users extends models.Model { _name = "res.users"; name = fields.Char(); _records = [ { id: 1, name: "Mitchell Admin" }, { id: 2, name: "Marc Demo" }, ]; } class Partner extends models.Model { _name = "res.partner"; name = fields.Char({ string: "Customer" }); partner_latitude = fields.Float({ string: "Latitude" }); partner_longitude = fields.Float({ string: "Longitude" }); contact_address_complete = fields.Char({ string: "Address" }); task_ids = fields.One2many({ string: "Task", relation: "project.task", relation_field: "partner_id", }); sequence = fields.Integer(); user_id = fields.Many2one({ string: "Salesperson", relation: "res.users", }); _records = [ { id: 1, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 1, user_id: 1, }, { id: 2, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 3, user_id: 2, }, { id: 3, name: "Bar", partner_latitude: 11.0, partner_longitude: 11.5, contact_address_complete: "Chaussée de Wavre 50, 1367, Ramillies", sequence: 4, user_id: false, }, ]; update_latitude_longitude() { return true; } } defineModels([Task, Users, Partner]); beforeEach(() => { patchWithCleanup(MapModel, { // set delay to 0 as _fetchCoordinatesFromAddressOSM is mocked COORDINATE_FETCH_DELAY: 0, }); patchWithCleanup(MapModel.prototype, { async _fetchCoordinatesFromAddressMB(metaData, data, record) { if (metaData.mapBoxToken !== MAP_BOX_TOKEN) { return Promise.reject({ status: 401 }); } const coordinates = []; coordinates[0] = "10.0"; coordinates[1] = "10.5"; const geometry = { coordinates }; const features = []; features[0] = { geometry }; const successResponse = { features }; const failResponse = { features: [] }; switch (record.contact_address_complete) { case "Cfezfezfefes": return failResponse; case "": return failResponse; } return successResponse; }, async _fetchCoordinatesFromAddressOSM(metaData, data, record) { const coordinates = []; coordinates[0] = { lat: "10.0", lon: "10.5" }; switch (record.contact_address_complete) { case "Cfezfezfefes": return []; case "": return []; } return coordinates; }, async _fetchRoute(metaData, data) { if (metaData.mapBoxToken !== MAP_BOX_TOKEN) { return Promise.reject({ status: 401 }); } const legs = []; for (let i = 1; i < data.records.length; i++) { const coordinates = []; coordinates[0] = [10, 10.5]; coordinates[1] = [10, 10.6]; const geometry = { coordinates }; const steps = []; steps[0] = { geometry }; legs.push({ steps: steps }); } const routes = []; routes[0] = { legs }; return { routes }; }, _notifyFetchedCoordinate(metaData, data) { // do not notify in tests as coords fetching is " synchronous " }, _openStreetMapAPI(metaData, data) { // return promise to wait for it return this._openStreetMapAPIAsync(metaData, data); }, }); }); describe.tags("desktop"); describe("map_view_desktop", () => { //-------------------------------------------------------------------------- // Testing data fetching //-------------------------------------------------------------------------- /** * data: no record * Should have no record * Should have no marker * Should have no route */ test("Create a view with no record", async () => { expect.assertions(6); patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = []; onRpc("project.task", "web_search_read", ({ kwargs }) => { const specification = kwargs.specification; expect(specification.partner_id).toEqual({ fields: { contact_address_complete: {}, display_name: {}, partner_latitude: {}, partner_longitude: {}, }, }); expect(specification.name).toEqual({}); }); onRpc("res.partner", "search_read", () => { throw new Error("Should not search_read the partners if there are no partner"); }); await mountView({ type: "map", resModel: "project.task", arch: ` `, }); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1", { message: "The link's URL should not contain any coordinates", } ); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "No marker should be on a the map.", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); }); /** * data: one record that has no partner linked to it * The record shouldn't be kept and displayed in the list of records * should have no marker * Should have no route */ test("Create a view with one record that has no partner", async () => { Task._records = TEST_RECORDS.task.recordWithouthPartner; Partner._records = []; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "No marker should be on a the map.", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); }); /** * data: one record that has a partner which has coordinates but no address * One record * One marker * no route */ test("Create a view with one record and a partner located by coordinates", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.coordinatesNoAddress; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be one marker on the map", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(1); }); /** * data: one record linked to one partner with no address and wrong coordinates * api: MapBox * record shouldn't be kept and displayed in the list * no route * no marker */ test("Create view with one record linked to a partner with wrong coordinates with MB", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.wrongCoordinatesNoAddress; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "There should be np marker on the map", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); }); /** * data: one record linked to one partner with no address and wrong coordinates * api: OpenStreet Map * record should be kept * no route * no marker */ test("Create view with one record linked to a partner with wrong coordinates with OSM", async () => { Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.wrongCoordinatesNoAddress; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "There should be no marker on the map", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); }); /** * data: one record linked to one partner with no coordinates and good address * api: OpenStreet Map * caching RPC called, assert good args * one record * no route */ test("Create View with one record linked to a partner with no coordinates and right address OSM", async () => { expect.assertions(5); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.noCoordinatesGoodAddress; onRpc("res.partner", "update_latitude_longitude", ({ args }) => { expect(args[0]).toHaveLength(1, { message: "There should be one record needing caching", }); expect(args[0][0].id).toBe(1, { message: "The records's id should be 1" }); return {}; }); await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(1); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be one marker on the map", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * data: one record linked to one partner with no coordinates and good address * api: MapBox * caching RPC called, assert good args * one record * no route */ test("Create View with one record linked to a partner with no coordinates and right address MB", async () => { expect.assertions(5); patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.noCoordinatesGoodAddress; onRpc("res.partner", "update_latitude_longitude", ({ args }) => { expect(args[0]).toHaveLength(1, { message: "There should be one record needing caching", }); expect(args[0][0].id).toBe(1, { message: "The records's id should be 1" }); return {}; }); await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(1); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be one marker on the map", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * data: one record linked to a partner with no coordinates and no address * api: MapBox * 1 record * no route * no marker */ test("Create view with no located record", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.unlocatedRecords; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "No marker should be on a the map.", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * data: one record linked to a partner with no coordinates and no address * api: OSM * one record * no route * no marker */ test("Create view with no located record OSM", async () => { Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.unlocatedRecords; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "No marker should be on a the map.", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * data: one record linked to a partner with no coordinates and wrong address * api: OSM * one record * no route * no marker */ test("Create view with no badly located record OSM", async () => { Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.noCoordinatesWrongAddress; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "No marker should be on a the map.", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * data: one record linked to a partner with no coordinates and wrong address * api: mapbox * one record * no route * no marker */ test("Create view with no badly located record MB", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.noCoordinatesWrongAddress; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(0); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "No marker should be on a the map.", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * data: 2 records linked to the same partner * 2 records * 2 markers * no route * same partner object * 1 caching request */ test("Create a view with two located records same partner", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecordOnePartner; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(2); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); expect(".leaflet-overlay-pane path").toHaveCount(1); }); /** * data: 2 records linked to differnet partners * 2 records * 1 route * different partner object. * 2 caching */ test("Create a view with two located records different partner", async () => { expect.assertions(5); patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; onRpc("res.partner", "update_latitude_longitude", ({ args }) => { expect(args[0]).toHaveLength(2, { message: "Should have 2 record needing caching", }); return {}; }); const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(2); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); expect(".leaflet-overlay-pane path").toHaveCount(1); expect(controller.model.data.records[0].partner).not.toBe( controller.model.data.records[1].partner, { message: "The records should have the same partner object as a property" } ); }); /** * data: 2 valid res.partner records * test the case where the model is res.partner and the "res.partner" field is the id * should have 2 records, * 2 markers * no route */ test("Create a view with res.partner", async () => { expect.assertions(4); patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Partner._records = [ { id: 2, name: "Foo", contact_address_complete: "Chaussée de Namur 40, 1367, Ramillies", sequence: 3, }, { id: 1, name: "FooBar", contact_address_complete: "Chaussée de Louvain 94, 5310 Éghezée", sequence: 1, }, ]; onRpc("res.partner", "web_search_read", ({ kwargs }) => { expect(kwargs.specification).toEqual({ contact_address_complete: {}, display_name: {}, id: {}, partner_latitude: {}, partner_longitude: {}, }); }); await mountView({ type: "map", resModel: "res.partner", arch: ``, }); expect( ".o-map-renderer--pin-list-container .o-map-renderer--pin-list-details li" ).toHaveCount(2); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); expect(".leaflet-overlay-pane path").toHaveCount(0); }); /** * Data: 3 partner records with user_id used as groupBy * Test if the map view displays the many2one field's name as the group name */ test("Create a view with many2one groupBy and res.partner model", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Partner._records = TEST_RECORDS.partner.threeRecords; await mountView({ type: "map", resModel: "res.partner", arch: ``, groupBy: ["user_id"], }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(3, { message: "Should have 3 groups", }); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual( ["Mitchell Admin", "Marc Demo", "None"], { message: "Should have correct group headers", } ); expect(".o-map-renderer--pin-list-details").toHaveCount(3, { message: "Should have 3 group detail sections", }); expect(".o-map-renderer--pin-list-details li").toHaveCount(3, { message: "Should have 3 total records across all groups", }); }); /** * data: 3 records linked to one located partner and one unlocated * test if only the 2 located records are displayed */ test("Create a view with 2 located records and 1 unlocated", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.threeRecords; Partner._records = TEST_RECORDS.partner.twoRecordsOneUnlocated; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.data.records.length).toBe(3); expect(controller.model.data.records[0].partner.id).toBe(1, { message: "The partner's id should be 1", }); expect(controller.model.data.records[1].partner.id).toBe(2, { message: "The partner's id should be 2", }); expect(controller.model.data.records[2].partner.id).toBe(1, { message: "The partner's id should be 1", }); }); test("Change load limit", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.threeRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(`.o_pager_counter .o_pager_value`).toHaveText("1-2"); expect(`.o_pager_counter span.o_pager_limit`).toHaveText("3"); }); //-------------------------------------------------------------------------- // Renderer testing //-------------------------------------------------------------------------- test("Google Maps redirection", async () => { Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&waypoints=Chauss%C3%A9e%20de%20Louvain%2094%2C%205310%20%C3%89ghez%C3%A9e|Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies", { message: "The link's URL should contain the address" } ); await contains(".leaflet-marker-icon").click(); expect("div.leaflet-popup a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&destination=Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies", { message: "The URL of the link should contain the address" } ); }); test("Google Maps redirection (with routing = true)", async () => { Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&destination=Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies&waypoints=Chauss%C3%A9e%20de%20Louvain%2094%2C%205310%20%C3%89ghez%C3%A9e", { message: "The link's URL should contain the address" } ); await contains(".leaflet-marker-icon").click(); expect("div.leaflet-popup a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&destination=Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies", { message: "The URL of the link should contain the address" } ); }); test("Unicity of coordinates in Google Maps url", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecordOnePartner; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&waypoints=Chauss%C3%A9e%20de%20Louvain%2094%2C%205310%20%C3%89ghez%C3%A9e", { message: "The link's URL should contain the address" } ); await contains(".leaflet-marker-icon").click(); expect("div.leaflet-popup a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&destination=Chauss%C3%A9e%20de%20Louvain%2094%2C%205310%20%C3%89ghez%C3%A9e", { message: "The URL of the link should contain the address" } ); }); test("test the position of pin", async () => { Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".o-map-renderer--marker").toHaveCount(1, { message: "Should have one marker created", }); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); const renderer = getMapRenderer(view); expect(renderer.markers[0].getLatLng().lat).toBe(10, { message: "The latitude should be the same as the record", }); expect(renderer.markers[0].getLatLng().lng).toBe(10.5, { message: "The longitude should be the same as the record", }); }); /** * data: two located records * Create an empty map */ test("Create of a empty map", async () => { Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; const view = await mountView({ type: "map", resModel: "res.partner", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.resPartnerField).toBe(null, { message: "the resPartnerField should not be set", }); expect(".o_map_view").toHaveClass("o_view_controller"); expect(".leaflet-map-pane").toHaveCount(1, { message: "If the map exists this div should exist", }); expect(".leaflet-pane .leaflet-tile-pane > *").toHaveCount(1, { message: "The map tiles should have been happened to the DOM", }); // if element o-map-renderer--container has class leaflet-container then // the map is mounted expect(".o-map-renderer--container").toHaveClass("leaflet-container", { message: "the map should be in the DOM", }); expect(".leaflet-overlay-pane > *").toHaveCount(0, { message: "Should have no showing route", }); }); /** * two located records * without routing or default_order * normal marker icon * test the click on them */ test("Create view with normal marker icons", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.numbering).toBe(false, { message: "the numbering option should not be enabled", }); expect(controller.model.metaData.routing).toBe(false, { message: "The routing option should not be enabled", }); expect(".leaflet-marker-icon").toHaveCount(1, { message: "There should be 1 marker" }); expect(".leaflet-overlay-pane path").toHaveCount(0); await contains(".leaflet-marker-icon").click(); expect(".leaflet-popup-pane > *").toHaveCount(1, { message: "Should have one showing popup", }); await contains("div.leaflet-container").click(); // wait for the popup's destruction which takes a certain time... for (let i = 0; i < 15; i++) { await animationFrame(); } expect(".leaflet-popup-pane > *").toHaveCount(0, { message: "Should not have any showing popup", }); }); /** * two located records * with default_order * no numbered icon * test click on them * asserts that the rpc receive the right parameters */ test("Create a view with default_order", async () => { expect.assertions(7); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; onRpc("project.task", "web_search_read", ({ kwargs }) => { expect(kwargs.order).toBe("name ASC", { message: "The sorting order should be on the field name in a ascendant way", }); }); const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.numbering).toBe(false, { message: "the numbering option should not be enabled", }); expect(controller.model.metaData.routing).toBe(false, { message: "The routing option should not be enabled", }); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be 1 marker" }); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); expect(".leaflet-popup-pane > *").toHaveCount(0, { message: "Should have no showing popup", }); await contains("div.leaflet-marker-icon").click(); expect(".leaflet-popup-pane > *").toHaveCount(1, { message: "Should have one showing popup", }); }); /** * two locted records * with routing enabled * numbered icon * test click on route */ test("Create a view with routing", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.numbering).toBe(true, { message: "the numbering option should be enabled", }); expect(controller.model.metaData.routing).toBe(true, { message: "The routing option should be enabled", }); expect(controller.model.data.numberOfLocatedRecords).toBe(2, { message: "Should have 2 located Records", }); expect(controller.model.data.routes.length).toBe(1, { message: "Should have 1 computed route", }); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); expect("path.leaflet-interactive").toHaveAttribute("stroke", "blue", { message: "The route should be blue if it has not been clicked", }); expect("path.leaflet-interactive").toHaveAttribute("stroke-opacity", "0.3", { message: "The opacity of the polyline should be 0.3", }); // the element isn't visible await contains("path.leaflet-interactive", { visible: false }).click(); await animationFrame(); expect("path.leaflet-interactive").toHaveAttribute("stroke", "darkblue", { message: "The route should be darkblue after being clicked", }); expect("path.leaflet-interactive").toHaveAttribute("stroke-opacity", "1", { message: "The opacity of the polyline should be 1", }); }); test("Resequence records", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); const taskRecords = [ { id: 1, name: "Project 1", sequence: 3, partner_id: 1, }, { id: 2, name: "Project 2", sequence: 4, partner_id: 1, }, { id: 3, name: "Project 3", sequence: 5, partner_id: 1, }, { id: 4, name: "Project 4", sequence: 6, partner_id: 1, }, { id: 5, name: "Project 5", sequence: 7, partner_id: 1, }, ]; Task._records = taskRecords; let resequenceCalled = false; patchWithCleanup(MapModel.prototype, { async _maxBoxAPI(metaData, data) { if (resequenceCalled) { // Verify that the API is called with the records correctly ordered const ids = data.records.map((record) => record.id); expect.step(`API called ${ids}`); } }, }); onRpc("web_resequence", ({ model, args, kwargs }) => { const [ids] = args; const { field_name: fieldName, offset } = kwargs; expect.step(`resequence ${model} ${fieldName} ${offset} ${ids}`); resequenceCalled = true; }); const view = await mountView({ type: "map", resModel: "project.task", arch: ` `, }); const controller = getMapController(view); expect(controller.model.metaData.allowResequence).toBe(true, { message: "The resequence option should be activated", }); expect(controller.model.metaData.defaultOrder[0].name).toBe("sequence"); expect(".o_row_handle").toHaveCount(5, { message: "Should have one row handle per record", }); expect(queryAllTexts(".o-map-renderer--pin-list-details li")).toEqual([ "1. Project 1", "2. Project 2", "3. Project 3", "4. Project 4", "5. Project 5", ]); await contains(".o-map-renderer--pin-located:nth-child(2) .o_row_handle").dragAndDrop( ".o-map-renderer--pin-located:nth-child(4) .o_row_handle" ); await animationFrame(); expect(queryAllTexts(".o-map-renderer--pin-list-details li")).toEqual([ "1. Project 1", "2. Project 3", "3. Project 4", "4. Project 2", "5. Project 5", ]); expect.verifySteps(["resequence project.task sequence 4 3,4,2", "API called 1,3,4,2,5"]); }); test("Resequence records, multiple default order fields", async () => { const taskRecords = [ { id: 1, name: "Project 1", sequence: 3, partner_id: 1, }, { id: 2, name: "Project 2", sequence: 3, partner_id: 1, }, ]; Task._records = taskRecords; const defer = new Deferred(); onRpc("web_resequence", async ({ model, args, kwargs }) => { const [ids] = args; const { field_name: fieldName, offset } = kwargs; await defer; expect.step(`resequence ${model} ${fieldName} ${offset} ${ids}`); }); await mountView({ type: "map", resModel: "project.task", arch: ` `, }); expect(queryAllTexts(".o-map-renderer--pin-list-details li")).toEqual([ "1. Project 1", "2. Project 2", ]); await contains(".o-map-renderer--pin-located:nth-child(1) .o_row_handle").dragAndDrop( ".o-map-renderer--pin-located:nth-child(2) .o_row_handle" ); await animationFrame(); // Model got notified before the backend resequence call expect(queryAllTexts(".o-map-renderer--pin-list-details li")).toEqual([ "1. Project 2", "2. Project 1", ]); defer.resolve(); await animationFrame(); expect.verifySteps(["resequence project.task sequence 3 2,1"]); }); test("When resequencing, model get notified before the backend call", async () => { const taskRecords = [ { id: 1, name: "Project 1", sequence: 3, partner_id: 1, }, { id: 2, name: "Project 2", sequence: 4, partner_id: 1, }, ]; Task._records = taskRecords; const defer = new Deferred(); onRpc("web_resequence", async ({ model, args, kwargs }) => { const [ids] = args; const { field_name: fieldName, offset } = kwargs; await defer; expect.step(`resequence ${model} ${fieldName} ${offset} ${ids}`); }); await mountView({ type: "map", resModel: "project.task", arch: ` `, }); expect(queryAllTexts(".o-map-renderer--pin-list-details li")).toEqual([ "1. Project 1", "2. Project 2", ]); await contains(".o-map-renderer--pin-located:nth-child(1) .o_row_handle").dragAndDrop( ".o-map-renderer--pin-located:nth-child(2) .o_row_handle" ); await animationFrame(); // Model got notified before the backend resequence call expect(queryAllTexts(".o-map-renderer--pin-list-details li")).toEqual([ "1. Project 2", "2. Project 1", ]); defer.resolve(); await animationFrame(); expect.verifySteps(["resequence project.task sequence 3 2,1"]); }); test("Create a view with routingError", async () => { expect.assertions(1); patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); patchWithCleanup(MapModel.prototype, { async _maxBoxAPI(metaData, data) { data.routingError = "this is test warning"; data.routes = []; }, }); Task._records = [ { id: 1, name: "FooProject", sequence: 1 }, { id: 2, name: "BarProject", sequence: 2 }, ]; Partner._records = []; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".o-map-renderer .o-map-renderer--alert").toHaveCount(1, { message: "should have alert", }); }); /** * routing with token and one located record * No route */ test("create a view with routing and one located record", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.routing).toBe(true, { message: "The routing option should be enabled", }); expect(controller.model.data.routes.length).toBe(0, { message: "Should have no computed route", }); }); /** * no mapbox token * assert that the view uses the right api and routes */ test("CreateView with empty mapbox token setting", async () => { Task._records = TEST_RECORDS.task.recordWithouthPartner; Partner._records = []; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.mapBoxToken).toBe("", { message: "The token should be an empty string", }); expect(controller.model.data.useMapBoxAPI).toBe(false, { message: "model should not use mapbox", }); }); /** * wrong mapbox token * assert that the view uses the openstreetmap api */ test("Create a view with wrong map box setting", async () => { patchWithCleanup(session, { map_box_token: "vrve" }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.mapBoxToken).toBe("vrve", { message: "The token should be kept", }); expect(controller.model.data.useMapBoxAPI).toBe(false, { message: "model should not use mapbox", }); }); /** * wrong mapbox token fails at catch at route computing */ test("create a view with wrong map box setting and located records", async () => { patchWithCleanup(session, { map_box_token: "frezfre" }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.mapBoxToken).toBe("frezfre", { message: "The token should be kept", }); expect(controller.model.data.useMapBoxAPI).toBe(false, { message: "model should not use mapbox", }); }); /** * create view with right map box token * assert that the view uses the map box api */ test("Create a view with the right map box token", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.recordWithouthPartner; Partner._records = []; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); expect(controller.model.metaData.mapBoxToken).toBe("token", { message: "The token should be the right token", }); expect(controller.model.data.useMapBoxAPI).toBe(true, { message: "model should not use mapbox", }); }); /** * data: two located records */ test("Click on pin shows popup, click on another shuts the first and open the other", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".leaflet-pane .leaflet-popup-pane > *").toHaveCount(0, { message: "The popup div should be empty", }); await contains("div.leaflet-marker-icon").click(); expect(".leaflet-popup-pane > *").toHaveCount(1, { message: "The popup div should contain one element", }); // the element isn't visible await contains(".leaflet-map-pane", { visible: false }).click(); await animationFrame(); // wait for the popup's destruction which takes a certain time... for (let i = 0; i < 15; i++) { await animationFrame(); } expect(".leaflet-pane .leaflet-popup-pane > *").toHaveCount(0, { message: "The popup div should be empty", }); }); /** * data: two located records * asserts that all the records are shown on the map */ test("assert that all the records are shown on the map", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); const mapX = queryOne(".leaflet-map-pane")._leaflet_pos.x; const mapY = queryOne(".leaflet-map-pane")._leaflet_pos.y; expect(mapX - queryOne("div.leaflet-marker-icon")._leaflet_pos.x).toBeLessThan(0, { message: "If the marker is currently shown on the map, the subtraction of latitude should be under 0", }); expect(mapY - queryOne("div.leaflet-marker-icon")._leaflet_pos.y).toBeLessThan(0); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); }); /** * data: two located records * asserts that the right fields are shown in the popup */ test("Content of the marker popup with one field", async () => { Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); const view = await mountView({ config: { views: [[false, "form"]] }, type: "map", resModel: "project.task", arch: ` `, }); const controller = getMapController(view); expect(controller.model.metaData.fieldNamesMarkerPopup[0].fieldName).toBe("name"); await contains("div.leaflet-marker-icon").click(); expect(controller.model.metaData.fieldNamesMarkerPopup.length).toBe(1, { message: "fieldsMarkerPopup should contain one field", }); expect("tbody tr").toHaveCount(1, { message: "The popup should have one field" }); expect("tbody tr").toHaveText("Name Foo", { message: "Field row's text should be 'Name Foo'", }); expect(".o-map-renderer--popup-buttons > *").toHaveCount(3, { message: "The popup should contain 2 buttons and one divider", }); }); test("Content of the marker popup with date time", async () => { mockTimeZone(+2); // UTC+2 patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecordsFieldDateTime; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates.map((r) => ({ ...r })); Partner._records[0].partner_latitude = 11.0; await mountView({ config: { views: [[false, "form"]] }, type: "map", resModel: "project.task", arch: ` `, }); await contains("div.leaflet-marker-icon:first-child").click(); expect("tbody tr .o-map-renderer--popup-table-content-value").toHaveCount(0, { message: "It should not contains a value node because it's not scheduled", }); await contains("div.leaflet-marker-icon:last-child").click(); expect("tbody tr .o-map-renderer--popup-table-content-value").toHaveText( "2022-02-07 23:09:31", { message: 'The time "2022-02-07 21:09:31" should be in the local timezone' } ); }); /** * data: two located records * asserts that no field is shown in popup */ test("Content of the marker with no field", async () => { Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressNoCoordinates; patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); await mountView({ config: { views: [[false, "form"]] }, type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect("tbody > *").toHaveCount(0, { message: "The popup should have only the button", }); expect(".o-map-renderer--popup-buttons > *").toHaveCount(3, { message: "The popup should contain 2 buttons and one divider", }); }); test("Attribute: hide_name", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect("tbody > tr").toHaveCount(1, { message: "The popup should have one field" }); expect("tbody tr .o-map-renderer--popup-table-content-name").toHaveText("Address", { message: "The popup should have address field", }); }); test("Render partner address field in popup", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect("tbody tr").toHaveCount(1, { message: "The popup should have one field" }); expect("tbody tr .o-map-renderer--popup-table-content-name").toHaveText("Address", { message: "The popup should have address field", }); expect("tbody tr .o-map-renderer--popup-table-content-value").toHaveText( "Chaussée de Namur 40, 1367, Ramillies", { message: "The popup should have correct address" } ); }); test("Hide partner address field in popup", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect("tbody tr").toHaveCount(1, { message: "The popup should have one field" }); expect("tbody tr .o-map-renderer--popup-table-content-name").toHaveText("Name", { message: "The popup should have name field", }); expect("tbody tr .o-map-renderer--popup-table-content-value").toHaveText("Foo", { message: "The popup should have correct address", }); }); test("Handle records of same co-ordinates in marker", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be a one marker", }); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); await contains("div.leaflet-marker-icon").click(); expect("tbody tr").toHaveCount(1, { message: "The popup should have one field" }); expect("tbody tr .o-map-renderer--popup-table-content-name").toHaveText("Address", { message: "The popup should have address field", }); }); test("Pager", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = Array.from({ length: 101 }, (_, index) => ({ id: index + 1, name: "project", partner_id: index + 1, })); Partner._records = Array.from({ length: 101 }, (_, index) => ({ id: index + 1, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, })); await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".o_pager").toHaveCount(1); expect(`.o_pager_counter .o_pager_value`).toHaveText("1-80", { message: "current pager value should be 1-20", }); expect(`.o_pager_counter span.o_pager_limit`).toHaveText("101", { message: "current pager limit should be 21", }); await contains(`.o_pager button.o_pager_next`).click(); expect(`.o_pager_counter .o_pager_value`).toHaveText("81-101", { message: "pager value should be 21-40", }); }); test("New domain", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = [ { id: 1, name: "FooProject", sequence: 1, partner_id: 1 }, { id: 2, name: "BarProject", sequence: 2, partner_id: 2 }, ]; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; const view = await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewArch: ` `, }); const controller = getMapController(view); expect(controller.model.data.records).toHaveLength(2, { message: "There should be 2 records", }); expect(".leaflet-overlay-pane path").toHaveCount(1); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); await toggleSearchBarMenu(); await toggleMenuItem("Filter 1"); expect(controller.model.data.records).toHaveLength(1, { message: "There should be 1 record", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be 1 marker on the map", }); await toggleMenuItem("Filter 1"); await toggleMenuItem("Filter 2"); expect(controller.model.data.records).toHaveLength(0, { message: "There should be no record", }); expect(".leaflet-overlay-pane path").toHaveCount(0); expect("div.leaflet-marker-icon").toHaveCount(0, { message: "There should be 0 marker on the map", }); await toggleMenuItem("Filter 2"); await toggleMenuItem("Filter 3"); expect(controller.model.data.records).toHaveLength(2, { message: "There should be 2 record", }); expect(".leaflet-overlay-pane path").toHaveCount(1); expect("div.leaflet-marker-icon").toHaveCount(1, { message: "There should be 1 marker on the map", }); expect("div.leaflet-marker-icon .o-map-renderer--marker-badge").toHaveText("2", { message: "There should be a marker for two records", }); }); test("Toggle grouped pin lists", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.threeRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, groupBy: ["partner_id"], }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(2, { message: "Should have 2 groups", }); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual(["Bar", "Foo"]); expect(".o-map-renderer--pin-list-details").toHaveCount(2); expect(".o-map-renderer--pin-list-details li").toHaveCount(3); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject\nFooBarProject", "BarProject", ]); await contains(".o-map-renderer--pin-list-group-header:eq(1)").click(); expect(".o-map-renderer--pin-list-group-header").toHaveCount(2, { message: "Should still have 2 groups", }); expect(".o-map-renderer--pin-list-details").toHaveCount(1); expect(".o-map-renderer--pin-list-details li").toHaveCount(2); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject\nFooBarProject", ]); await contains(".o-map-renderer--pin-list-group-header:eq(0)").click(); expect(".o-map-renderer--pin-list-details").toHaveCount(0); await contains(".o-map-renderer--pin-list-group-header:eq(1)").click(); expect(".o-map-renderer--pin-list-details").toHaveCount(1); expect(".o-map-renderer--pin-list-details li").toHaveCount(1); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual(["BarProject"]); }); test("Toggle grouped one2many pin lists", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.threeRecords.map((r) => ({ ...r })); Task._records[1].partner_ids = [1, 3]; await mountView({ type: "map", resModel: "project.task", arch: ``, groupBy: ["partner_ids"], }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(3, { message: "Should have 3 groups", }); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual([ "Foo", "Foo", "Bar", ]); expect(".o-map-renderer--pin-list-details").toHaveCount(3); expect(".o-map-renderer--pin-list-details li").toHaveCount(5); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject\nBarProject\nFooBarProject", "FooProject", "BarProject", ]); expect(".leaflet-marker-icon").toHaveCount(3); }); test("Check groupBy on datetime field", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._fields["scheduled_date"] = fields.Datetime({ string: "Schedule date", }); Task._records = [ { id: 1, name: "FooProject", sequence: 1, partner_id: 1, scheduled_date: false }, ]; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewId: false, searchViewArch: ` `, }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(0, { message: "Should not have any groups", }); await toggleSearchBarMenu(); // don't throw an error when grouping a field with a false value await toggleMenuItem("scheduled_date"); await toggleMenuItemOption("scheduled_date", "year"); }); test("Check groupBy on properties field", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Partner._fields["properties_definition"] = fields.PropertiesDefinition({ string: "Properties Definition", }); Task._fields["task_properties"] = fields.Properties({ string: "Properties", definition_record: "partner_id", definition_record_field: "properties_definition", }); Partner._records = [ { id: 1, name: "Bar", }, { id: 2, name: "Foo", partner_latitude: 10.0, partner_longitude: 10.5, sequence: 3, properties_definition: [ { name: "bd6404492c244cff", type: "char", string: "Reference Number", }, ], }, ]; Task._records = [ { id: 1, name: "FooProject", sequence: 1, partner_id: 1, task_properties: { bd6404492c244cff: "1234", }, }, ]; await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewId: false, searchViewArch: ` `, }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(0, { message: "Should not have any groups", }); await toggleSearchBarMenu(); // don't throw an error when grouping on a property await toggleMenuItem("task_properties"); await animationFrame(); await contains(".o_accordion_values .o_menu_item").click(); // check that the property has been added in the facet without crashing expect(`.o_facet_value:contains("Reference Number")`).toHaveCount(1); expect(".o-map-renderer--pin-list-group-header").toHaveCount(0); }); test("Change groupBy", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.threeRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewId: false, searchViewArch: ` `, }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(0, { message: "Should not have any groups", }); await toggleSearchBarMenu(); await toggleMenuItem("Partner"); expect(".o-map-renderer--pin-list-group-header").toHaveCount(2, { message: "Should have 2 groups", }); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual(["Bar", "Foo"]); // Groups should be loaded too expect(".o-map-renderer--pin-list-details li").toHaveCount(3); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject\nFooBarProject", "BarProject", ]); await toggleMenuItem("Name"); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual(["Bar", "Foo"], { message: "Should not have changed", }); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject\nFooBarProject", "BarProject", ]); await toggleMenuItem("Partner"); expect(".o-map-renderer--pin-list-group-header").toHaveCount(3, { message: "Should have 3 groups", }); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual([ "FooProject", "BarProject", "FooBarProject", ]); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject", "BarProject", "FooBarProject", ]); expect(".o-map-renderer--pin-list-details:eq(0) li").toHaveCount(1); expect(".o-map-renderer--pin-list-details:eq(1) li").toHaveCount(1); expect(".o-map-renderer--pin-list-details:eq(2) li").toHaveCount(1); }); //-------------------------------------------------------------------------- // Controller testing //-------------------------------------------------------------------------- test("Click on open button switches to form view", async () => { mockService("action", { switchView(name, props) { expect.step("switchView"); expect(name).toBe("form", { message: "The view switched to should be form" }); expect(props).toEqual({ resId: 1 }, { message: "Props should be correct" }); }, }); Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ config: { views: [[false, "form"]] }, type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect( "div.leaflet-popup-pane button.btn.btn-primary.o-map-renderer--popup-buttons-open" ).toHaveCount(1, { message: "The button should be present in the dom" }); await contains( "div.leaflet-popup-pane button.btn.btn-primary.o-map-renderer--popup-buttons-open" ).click(); expect.verifySteps(["switchView"]); }); test("Middle click on open button open record in tab", async () => { mockService("action", { switchView(name, props, options) { expect.step("switchView"); expect(name).toBe("form", { message: "The view switched to should be form" }); expect(props).toEqual({ resId: 1 }, { message: "Props should be correct" }); expect(options).toEqual({ newWindow: true }); }, }); Task._views.form = "
"; Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ config: { views: [[false, "form"]] }, type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect( "div.leaflet-popup-pane button.btn.btn-primary.o-map-renderer--popup-buttons-open" ).toHaveCount(1, { message: "The button should be present in the dom" }); await contains( "div.leaflet-popup-pane button.btn.btn-primary.o-map-renderer--popup-buttons-open" ).click({ ctrlKey: true }); expect.verifySteps(["switchView"]); }); test("Test the lack of open button", async () => { Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect( "div.leaflet-popup-pane button.btn.btn-primary.o-map-renderer--popup-buttons-open" ).toHaveCount(0, { message: "The button should not be present in the dom" }); }); test("attribute panel_title on the arch should display in the pin list", async () => { Task._records = TEST_RECORDS.task.oneRecord; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".o-map-renderer--pin-list-container .o_pin_list_header span").toHaveText( "AAAAAAAAAAAAAAAAA" ); }); test("Test using a field other than partner_id for the map view", async () => { Task._records = TEST_RECORDS.task.anotherPartnerId; Partner._records = TEST_RECORDS.partner.oneLocatedRecord; await mountView({ type: "map", resModel: "project.task", arch: ``, }); await contains("div.leaflet-marker-icon").click(); expect( "div.leaflet-popup-pane button.btn.btn-primary.o-map-renderer--popup-buttons-open" ).toHaveCount(0, { message: "The button should not be present in the dom" }); }); test("Check Google Maps URL is updating on domain change", async () => { Task._records = [ { id: 1, name: "FooProject", sequence: 1, partner_id: 2 }, { id: 2, name: "BarProject", sequence: 2, partner_id: 3 }, ]; await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewArch: ` `, }); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&waypoints=Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies|Chauss%C3%A9e%20de%20Wavre%2050%2C%201367%2C%20Ramillies", { message: "The link's URL initially should contain the addresses for all records" } ); //apply domain and check that the Google Maps URL on the button reflects the changes await toggleSearchBarMenu(); await toggleMenuItem("FooProject only"); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&waypoints=Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies", { message: "The link's URL after domain is applied should only contain addresses for filtered records", } ); }); test("Check Google Maps URL (routing and multiple records)", async () => { Task._records = [ { id: 1, name: "FooProject", sequence: 1, partner_id: 2 }, { id: 2, name: "BarProject", sequence: 2, partner_id: 3 }, ]; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect("a.btn.btn-primary").toHaveAttribute( "href", "https://www.google.com/maps/dir/?api=1&destination=Chauss%C3%A9e%20de%20Wavre%2050%2C%201367%2C%20Ramillies&waypoints=Chauss%C3%A9e%20de%20Namur%2040%2C%201367%2C%20Ramillies", { message: "The link's URL initially should contain the addresses for all records" } ); }); test("Do not notify if unmounted after fetching coordinate", async () => { const def = new Deferred(); patchWithCleanup(MapModel.prototype, { _fetchCoordinatesFromAddressOSM() { return def; }, _notifyFetchedCoordinate() { expect.step("notify"); }, }); const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); destroy(controller); def.resolve(); await animationFrame(); expect.verifySteps([]); }); test("Do not fetch if unmounted after waiting interval", async () => { patchWithCleanup(MapModel.prototype, { async _fetchCoordinatesFromAddressOSM() { expect.step("_fetchCoordinatesFromAddressOSM"); }, _notifyFetchedCoordinate() { expect.step("_notifyFetchedCoordinate"); }, _openStreetMapAPIAsync() { expect.step("_openStreetMapAPIAsync"); return super._openStreetMapAPIAsync(...arguments); }, }); const view = await mountView({ type: "map", resModel: "project.task", arch: ``, }); const controller = getMapController(view); destroy(controller); await animationFrame(); expect.verifySteps(["_openStreetMapAPIAsync"]); }); test("Check groupBy on selection field", async () => { expect.assertions(1); patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = [ { id: 1, name: "Project", sequence: 1, partner_id: 1, task_status: "abc" }, ]; onRpc( "res.partner", "search_read", () => TEST_RECORDS.partner.twoRecordsAddressCoordinates ); await mountView({ type: "map", resModel: "project.task", arch: ``, groupBy: ["task_status"], }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(1, { message: "ABC" }); }); test("display '0' for false group, when grouped by int field", async () => { await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewId: false, groupBy: ["sequence"], }); expect(".o-map-renderer--pin-list-group-header").toHaveText("0"); }); test("Map view with default_group_by", async () => { Task._records = TEST_RECORDS.task.threeRecords; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(2, { message: "Should have 2 groups", }); expect(queryAllTexts(".o-map-renderer--pin-list-group-header")).toEqual(["Bar", "Foo"]); expect(".o-map-renderer--pin-list-details").toHaveCount(2); expect(".o-map-renderer--pin-list-details li").toHaveCount(3); expect(queryAllTexts(".o-map-renderer--pin-list-details")).toEqual([ "FooProject\nFooBarProject", "BarProject", ]); }); test("GroupBy on datetime field with no subgroup specified", async () => { patchWithCleanup(session, { map_box_token: MAP_BOX_TOKEN }); Task._records = TEST_RECORDS.task.twoRecordsFieldDateTime; Partner._records = TEST_RECORDS.partner.twoRecordsAddressCoordinates; await mountView({ type: "map", resModel: "project.task", arch: ``, }); expect(".o-map-renderer--pin-list-group-header").toHaveCount(2); expect(".o-map-renderer--pin-list-group-header:eq(1)").toHaveText("February 2022", { message: "Should default to month scale when not specified", }); }); test("display the field's falsy_value_label for false group, if defined", async () => { Task._fields.partner_id.falsy_value_label = "I'm the false group"; Task._records[0].partner_id = false; await mountView({ type: "map", resModel: "project.task", arch: ``, searchViewId: false, groupBy: ["partner_id"], }); expect(".o-map-renderer--pin-list-group-header").toHaveText("I'm the false group"); }); }); describe.tags("mobile"); describe("map_view_mobile", () => { test("use pin list container on mobile", async () => { await mountView({ type: "map", resModel: "project.task", arch: ` `, }); expect(".o_pin_list_header .fa.fa-caret-left").toHaveCount(1); expect(".o_pin_list_header .fa.fa-caret-down").toHaveCount(0); await contains(".o_pin_list_header").click(); expect(".o-sm-pin-list-container.h-100").toHaveCount(1, { message: "There should extend the pin list container", }); expect(".o_pin_list_header .fa.fa-caret-down").toHaveCount(1); expect(".o_pin_list_header .fa.fa-caret-left").toHaveCount(0); await contains(".o_pin_list_header").click(); expect(".o_pin_list_header .fa.fa-caret-left").toHaveCount(1); expect(".o_pin_list_header .fa.fa-caret-down").toHaveCount(0); expect(".o-sm-pin-list-container.h-100").toHaveCount(0, { message: "There should collapse the pin list container", }); }); });