import i18next from "i18next"; import { observable, makeObservable } from "mobx"; import { http, passthrough } from "msw"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import TimeInterval from "terriajs-cesium/Source/Core/TimeInterval"; import ConstantProperty from "terriajs-cesium/Source/DataSources/ConstantProperty"; import Entity from "terriajs-cesium/Source/DataSources/Entity"; import PropertyBag from "terriajs-cesium/Source/DataSources/PropertyBag"; import TimeIntervalCollectionProperty from "terriajs-cesium/Source/DataSources/TimeIntervalCollectionProperty"; import CatalogMemberMixin, { getName } from "../../lib/ModelMixins/CatalogMemberMixin"; import DiscretelyTimeVaryingMixin from "../../lib/ModelMixins/DiscretelyTimeVaryingMixin"; import MappableMixin, { MapItem } from "../../lib/ModelMixins/MappableMixin"; import CzmlCatalogItem from "../../lib/Models/Catalog/CatalogItems/CzmlCatalogItem"; import CatalogMemberFactory from "../../lib/Models/Catalog/CatalogMemberFactory"; import CommonStrata from "../../lib/Models/Definition/CommonStrata"; import CreateModel from "../../lib/Models/Definition/CreateModel"; import { ModelConstructorParameters } from "../../lib/Models/Definition/Model"; import upsertModelFromJson from "../../lib/Models/Definition/upsertModelFromJson"; import TerriaFeature from "../../lib/Models/Feature/Feature"; import Terria from "../../lib/Models/Terria"; import ViewState from "../../lib/ReactViewModels/ViewState"; import { FeatureInfoSection } from "../../lib/ReactViews/FeatureInfo/FeatureInfoSection"; import DiscretelyTimeVaryingTraits from "../../lib/Traits/TraitsClasses/DiscretelyTimeVaryingTraits"; import FeatureInfoUrlTemplateTraits from "../../lib/Traits/TraitsClasses/FeatureInfoTraits"; import MappableTraits from "../../lib/Traits/TraitsClasses/MappableTraits"; import mixTraits from "../../lib/Traits/mixTraits"; import * as FeatureInfoPanel from "../../lib/ViewModels/FeatureInfoPanel"; import { renderWithContexts } from "./withContext"; import CsvCatalogItem from "../../lib/Models/Catalog/CatalogItems/CsvCatalogItem"; import updateModelFromJson from "../../lib/Models/Definition/updateModelFromJson"; import { cleanup, screen, within } from "@testing-library/react"; import { act } from "react"; import { worker } from "../mocks/browser"; import json from "../../wwwroot/test/init/czml-with-template-0.json"; let separator = ","; if (typeof Intl === "object" && typeof Intl.NumberFormat === "function") { const thousand = Intl.NumberFormat().format(1000); if (thousand.length === 5) { separator = thousand[1]; } } // Takes the absolute value of the value and pads it to 2 digits i.e. 7->07, 17->17, -3->3, -13->13. It is expected that value is an integer is in the range [0, 99]. function absPad2(value: number) { return (Math.abs(value) < 10 ? "0" : "") + Math.abs(value); } describe("FeatureInfoSection", function () { let terria: Terria; let feature: TerriaFeature; let viewState: ViewState; let catalogItem: TestModel; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); catalogItem = new TestModel("teststrata", terria); viewState = new ViewState({ terria, catalogSearchProvider: undefined }); const properties = { name: "Kay", foo: "bar", material: "steel", "material.process.#1": "smelted", size: 12345678.9012, efficiency: "0.2345678", date: "2017-11-23T08:47:53Z", owner_html: "Jay
Smith", ampersand: "A & B", lessThan: "A < B", unsafe: 'ok!' }; feature = new TerriaFeature({ name: "Bar", properties: properties }); feature._catalogItem = catalogItem; }); it("renders a static description", function () { feature.description = new ConstantProperty("

hi!

"); renderWithContexts( {}} />, viewState ); expect(screen.getByText("hi!")).toBeVisible(); }); it("does not render unsafe html", function () { feature.description = new ConstantProperty( '

hi!

' ); const { container } = renderWithContexts( {}} />, viewState ); expect(container.querySelectorAll("script").length).toEqual(0); expect(screen.queryByText(/alert\("gotcha"\)/)).not.toBeInTheDocument(); expect(screen.getByText("hi!")).toBeVisible(); }); function timeVaryingDescription() { const desc = new TimeIntervalCollectionProperty(); desc.intervals.addInterval( new TimeInterval({ start: JulianDate.fromDate(new Date("2010-01-01")), stop: JulianDate.fromDate(new Date("2011-01-01")), data: "

hi

" }) ); desc.intervals.addInterval( new TimeInterval({ start: JulianDate.fromDate(new Date("2011-01-01")), stop: JulianDate.fromDate(new Date("2012-01-01")), data: "

bye

" }) ); return desc; } it("renders a time-varying description", function () { feature.description = timeVaryingDescription(); catalogItem.setTrait(CommonStrata.user, "currentTime", "2011-06-30"); const { rerender } = renderWithContexts( {}} />, viewState ); expect(screen.queryByText("hi")).not.toBeInTheDocument(); expect(screen.getByText("bye")).toBeVisible(); act(() => { catalogItem.setTrait(CommonStrata.user, "currentTime", "2010-06-30"); }); rerender( {}} /> ); expect(screen.getByText("hi")).toBeVisible(); expect(screen.queryByText("bye")).not.toBeInTheDocument(); }); it("handles features with no properties", function () { feature = new Entity({ name: "Foot", description: "bart" }); renderWithContexts( {}} />, viewState ); expect(screen.getByText("bart")).toBeVisible(); expect(screen.getByText(`${getName(catalogItem)} - Foot`)).toBeVisible(); }); it("handles html format feature info", function () { feature = new Entity({ name: "Foo", description: "GetFeatureInfo
thing
BAR

" }); renderWithContexts( {}} />, viewState ); expect(screen.getByText(`${getName(catalogItem)} - Foo`)).toBeVisible(); expect(screen.queryByText("GetFeatureInfo")).not.toBeInTheDocument(); expect(screen.getByText("BAR")).toBeVisible(); }); it("handles html format feature info where markdown would break the html", function () { feature = new Entity({ name: "Foo", description: "GetFeatureInfo\n\n \n\n
thing
BAR

" }); // Markdown applied to this description would pull out the lonely and make it
\n
, so check this doesn't happen. renderWithContexts( {}} />, viewState ); expect(screen.queryByText("\n")).not.toBeInTheDocument(); expect(screen.queryByText("<\n")).not.toBeInTheDocument(); }); it("maintains and applies inline style attributes", function () { feature = new Entity({ name: "Foo", description: '
countdown
' }); renderWithContexts( {}} />, viewState ); const styledDiv = screen.getByText("countdown"); expect(styledDiv.style.background.replace(/ /g, "")).toEqual( "rgb(170,187,204)" ); }); it("does not break when html format feature info has style tag", function () { feature = new Entity({ name: "Foo", description: 'GetFeatureInfo
thing
BAR

' }); renderWithContexts( {}} />, viewState ); expect(screen.getByText(`${getName(catalogItem)} - Foo`)).toBeVisible(); expect(screen.queryByText("GetFeatureInfo")).not.toBeInTheDocument(); expect(screen.getByText("BAR")).toBeVisible(); }); it("does not break when there are neither properties nor description", function () { feature = new Entity({ name: "Vapid" }); renderWithContexts( s} />, viewState ); expect(screen.getByText(`${getName(catalogItem)} - Vapid`)).toBeVisible(); expect(screen.getByText("featureInfo.noInfoAvailable")).toBeVisible(); }); it("does not break when a template name needs to be rendered but no properties are set", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.user, "name", "Title {{name}}" ); feature = new Entity(); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Title")).toBeVisible(); }); it("shows properties if no description", function () { feature = new Entity({ name: "Meals", properties: { lunch: "eggs", dinner: { getValue: function () { return "ham"; } } } }); renderWithContexts( {}} />, viewState ); expect(screen.getByText(`${getName(catalogItem)} - Meals`)).toBeVisible(); expect(screen.getByText("lunch")).toBeVisible(); expect(screen.getByText("eggs")).toBeVisible(); expect(screen.getByText("dinner")).toBeVisible(); expect(screen.getByText("ham")).toBeVisible(); }); it("gracefully handles bad nested JSON", function () { feature = new Entity({ name: "Meals", properties: { somethingBad: "{broken object", somethingGood: JSON.stringify({ good: "this object is good" }) } }); renderWithContexts( {}} />, viewState ); expect(screen.getByText("{broken object")).toBeVisible(); expect(screen.getByText(`{"good":"this object is good"}`)).toBeVisible(); }); describe("templating", function () { it("uses and completes a string-form featureInfoTemplate if present", function () { const template = "This is a {{material}} {{foo}}."; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("This is a steel bar.")).toBeInTheDocument(); }); it("uses activeStyle of catalog item having TableTraits in featureInfoTemplate", function () { const csvItem = new CsvCatalogItem("testId", terria, undefined); csvItem.setTrait(CommonStrata.user, "activeStyle", "User Style"); const styles = [ { id: "User Style", color: { colorColumn: "ste_name", colorPalette: "HighContrast" }, hidden: false }, { id: "Other Style", color: { colorColumn: "other", colorPalette: "HighContrast" }, hidden: false } ]; updateModelFromJson(csvItem, CommonStrata.user, { styles }); const template = "The active style id is {{terria.activeStyle.id}}."; csvItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect( screen.getByText("The active style id is User Style.") ).toBeInTheDocument(); }); it("can use _ to refer to . and # in property keys in the featureInfoTemplate", function () { const template = "Made from {{material_process__1}} {{material}}."; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Made from smelted steel.")).toBeInTheDocument(); }); it("formats large numbers without commas", function () { const template = "Size: {{size}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Size: 12345678.9012")).toBeInTheDocument(); }); it("can format numbers with commas", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "Size: {{size}}" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "formats", { size: { type: "number", useGrouping: true } } as any ); renderWithContexts( {}} />, viewState ); expect( screen.getByText( "Size: 12" + separator + "345" + separator + "678.9012" ) ).toBeVisible(); }); it("formats numbers in the formats section with no type as if type were number", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "Size: {{size}}" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "formats", { size: { useGrouping: true } } as any ); renderWithContexts( {}} />, viewState ); expect( screen.getByText( "Size: 12" + separator + "345" + separator + "678.9012" ) ).toBeVisible(); }); it("can format numbers using terria.formatNumber", function () { let template = 'Base: {{#terria.formatNumber}}{"useGrouping":false}{{size}}{{/terria.formatNumber}}'; template += '; Sep: {{#terria.formatNumber}}{"useGrouping":true, "maximumFractionDigits":3}{{size}}{{/terria.formatNumber}}'; template += '; DP: {{#terria.formatNumber}}{"maximumFractionDigits":3}{{efficiency}}{{/terria.formatNumber}}'; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect( screen.getByText("Base: 12345678.9012; Sep: 12,345,678.901; DP: 0.235") ).toBeVisible(); }); it("can format numbers using terria.formatNumber without quotes", function () { let template = "Sep: {{#terria.formatNumber}}{useGrouping:true, maximumFractionDigits:3}{{size}}{{/terria.formatNumber}}"; template += "; DP: {{#terria.formatNumber}}{maximumFractionDigits:3}{{efficiency}}{{/terria.formatNumber}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Sep: 12,345,678.901; DP: 0.235")).toBeVisible(); }); it("can handle white text in terria.formatNumber", function () { const template = 'Sep: {{#terria.formatNumber}}{"useGrouping":true, "maximumFractionDigits":3} \n {{size}}{{/terria.formatNumber}}'; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect( screen.getByText("Sep: 12" + separator + "345" + separator + "678.901") ).toBeVisible(); }); it("handles non-numbers terria.formatNumber", function () { const template = "Test: {{#terria.formatNumber}}text{{/terria.formatNumber}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Test: text")).toBeInTheDocument(); }); it("can use a dateFormatString when it is specified in terria.formatDateTime", function () { const template = 'Test: {{#terria.formatDateTime}}{"format": "dd-mm-yyyy HH:MM:ss"}2017-11-23T08:47:53Z{{/terria.formatDateTime}}'; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); const date = new Date(Date.UTC(2017, 11, 23, 8, 47, 53)); const formattedDate = absPad2(date.getDate()) + "-" + absPad2(date.getMonth()) + "-" + date.getFullYear() + " " + absPad2(date.getHours()) + ":" + absPad2(date.getMinutes()) + ":" + absPad2(date.getSeconds()); expect(screen.getByText("Test: " + formattedDate)).toBeInTheDocument(); }); it("defaults dateFormatString to isoDateTime when it is not specified in terria.formatDateTime", function () { const template = "Test: {{#terria.formatDateTime}}2017-11-23T08:47:53Z{{/terria.formatDateTime}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); const date = new Date(Date.UTC(2017, 11, 23, 8, 47, 53)); const offset = -date.getTimezoneOffset(); const offsetMinute = offset % 60; const offsetHour = (offset - offsetMinute) / 60; const timeZone = (offset >= 0 ? "+" : "-") + absPad2(offsetHour) + "" + absPad2(offsetMinute); const formattedDate = date.getFullYear() + "-" + absPad2(date.getMonth()) + "-" + absPad2(date.getDate()) + "T" + absPad2(date.getHours()) + ":" + absPad2(date.getMinutes()) + ":" + absPad2(date.getSeconds()) + timeZone; expect(screen.getByText("Test: " + formattedDate)).toBeInTheDocument(); }); it("can format dates using the dateTime as the type within the formats section", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "Date: {{date}}" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "formats", { date: { type: "dateTime", format: "dd-mm-yyyy HH:MM:ss" } } as any ); renderWithContexts( {}} />, viewState ); const date = new Date(Date.UTC(2017, 11, 23, 8, 47, 53)); const formattedDate = absPad2(date.getDate()) + "-" + absPad2(date.getMonth()) + "-" + date.getFullYear() + " " + absPad2(date.getHours()) + ":" + absPad2(date.getMinutes()) + ":" + absPad2(date.getSeconds()); expect(screen.getByText("Date: " + formattedDate)).toBeInTheDocument(); }); it("handles non-numbers in terria.formatDateTime", function () { const template = "Test: {{#terria.formatDateTime}}text{{/terria.formatDateTime}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Test: text")).toBeInTheDocument(); }); it("url encodes text components", function () { const template = "Test: {{#terria.urlEncodeComponent}}W/HO:E#1{{/terria.urlEncodeComponent}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Test: W%2FHO%3AE%231")).toBeInTheDocument(); }); it("url encodes sections of text", function () { const template = "Test: {{#terria.urlEncode}}http://example.com/a b{{/terria.urlEncode}}"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); const { container } = renderWithContexts( {}} />, viewState ); expect(within(container).getByRole("link")).toHaveAttribute( "href", "http://example.com/a%20b" ); }); it("does not escape ampersand as &", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "Ampersand: {{ampersand}}" ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Ampersand: A & B")).toBeInTheDocument(); expect(screen.queryByText(/&/)).not.toBeInTheDocument(); }); it("does not escape < as <", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "Less than: {{lessThan}}" ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Less than: A < B")).toBeInTheDocument(); expect(screen.queryByText(/</)).not.toBeInTheDocument(); }); it("can embed safe html in template", function () { const template = "
Hello {{owner_html}}.
"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); const { container } = renderWithContexts( {}} />, viewState ); expect(screen.getByText(/Hello Jay/)).toBeInTheDocument(); expect(container.querySelectorAll("br").length).toEqual(1); expect(screen.getByText(/Smith\./)).toBeInTheDocument(); }); it("cannot embed unsafe html in template", function () { const template = "
Hello {{unsafe}}
"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); const { container } = renderWithContexts( {}} />, viewState ); expect(screen.getByText(/Hello ok!/)).toBeInTheDocument(); expect(container.querySelectorAll("script").length).toEqual(0); expect(screen.queryByText(/alert\("gotcha"\)/)).not.toBeInTheDocument(); }); it("can use a json featureInfoTemplate with partials", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", '
test {{>boldfoo}}
' ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "partials", { boldfoo: "{{foo}}" } ); const { container } = renderWithContexts( {}} />, viewState ); expect(container.querySelectorAll(".jk").length).toEqual(0); expect(container.querySelectorAll(".jj").length).toEqual(1); expect(container.querySelectorAll("b").length).toEqual(1); expect(screen.getByText(/bar/)).toBeInTheDocument(); expect(container.textContent).toContain("test "); }); it("sets the name from featureInfoTemplate", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "name", "{{name}} {{foo}}" ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Kay bar")).toBeInTheDocument(); }); it("can access clicked lat and long", function () { const template = "
Clicked {{#terria.formatNumber}}{maximumFractionDigits:0}{{terria.coords.latitude}}{{/terria.formatNumber}}, {{#terria.formatNumber}}{maximumFractionDigits:0}{{terria.coords.longitude}}{{/terria.formatNumber}}
"; const position = Ellipsoid.WGS84.cartographicToCartesian( Cartographic.fromDegrees(77, 44, 6) ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Clicked 44, 77")).toBeInTheDocument(); }); it("can replace text, using terria.partialByName", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "{{#terria.partialByName}}{{name}}{{/terria.partialByName}}" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "partials", { Bar: "Rab", Kay: "Yak", "This name": "That name" } ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Yak")).toBeInTheDocument(); expect(screen.queryByText("Kay")).not.toBeInTheDocument(); cleanup(); feature.properties = new PropertyBag({ name: "This name" }); renderWithContexts( {}} />, viewState ); expect(screen.getByText("That name")).toBeInTheDocument(); expect(screen.queryByText("Yak")).not.toBeInTheDocument(); }); it("does not replace text if no matching, using terria.partialByName", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "{{#terria.partialByName}}{{name}}{{/terria.partialByName}}" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "partials", { Bar: "Rab", NotKay: "Yak", "This name": "That name" } ); renderWithContexts( {}} />, viewState ); expect(screen.queryByText("Yak")).not.toBeInTheDocument(); expect(screen.getByText("Kay")).toBeInTheDocument(); }); it("can replace text and filter out unsafe replacement, using terria.partialByName", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "{{#terria.partialByName}}{{name}}{{/terria.partialByName}}" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "partials", { Bar: "Rab", Kay: "Yak!", This: "That" } ); renderWithContexts( {}} />, viewState ); expect(screen.getByText("Yak!")).toBeInTheDocument(); expect(screen.queryByText(/Yak!alert/)).not.toBeInTheDocument(); expect(screen.queryByText(/alert\('gotcha'\)/)).not.toBeInTheDocument(); expect(screen.queryByText("Kay")).not.toBeInTheDocument(); }); it("can access the current time", function () { const template = "
Time: {{terria.currentTime}}
"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); catalogItem._discreteTimes = ["2017-11-23", "2018-01-03"]; catalogItem.setTrait(CommonStrata.user, "currentTime", "2017-12-01"); terria.timelineClock.currentTime = JulianDate.fromIso8601( "2001-01-01T01:01:01+01:00" ); renderWithContexts( {}} />, viewState ); const expectedTime = new Date(catalogItem._discreteTimes[0]).toString(); expect(screen.getByText(`Time: ${expectedTime}`)).toBeInTheDocument(); }); it("can render a recursive featureInfoTemplate", function () { catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", "
    {{>show_children}}
" ); catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "partials", { show_children: "{{#children}}
  • {{name}}
      {{>show_children}}
  • {{/children}}" } ); feature.properties?.merge({ children: [ { name: "Alice", children: [ { name: "Bailey", children: null }, { name: "Beatrix", children: null } ] }, { name: "Xavier", children: [ { name: "Yann", children: null }, { name: "Yvette", children: null } ] } ] }); const { container } = renderWithContexts( {}} />, viewState ); expect(within(container).getAllByRole("list").length).toBe(7); expect(within(container).getAllByRole("listitem").length).toBe(7); }); }); describe("raw data", function () { beforeEach(function () { feature.description = new ConstantProperty("

    hi!

    "); }); it("does not appear if no template", function () { renderWithContexts( , viewState ); expect( screen.queryByText(/featureInfo\.showCuratedData/) ).not.toBeInTheDocument(); expect( screen.queryByText(/featureInfo\.showRawData/) ).not.toBeInTheDocument(); }); it('shows "Show Raw Data" if template', function () { const template = "Test"; catalogItem.featureInfoTemplate.setTrait( CommonStrata.definition, "template", template ); renderWithContexts( , viewState ); expect( screen.queryByText(/featureInfo\.showCuratedData/) ).not.toBeInTheDocument(); expect(screen.getByText("featureInfo.showRawData")).toBeInTheDocument(); }); }); describe("CZML templating", function () { beforeEach(function () { worker.use( http.get("test/CZML/withProperties.czml", () => passthrough()), http.get("test/CZML/withTimeVaryingProperties.czml", () => passthrough() ) ); }); it("uses and completes a string-form featureInfoTemplate", async function () { const czmlItem = upsertModelFromJson( CatalogMemberFactory, terria, "", "definition", json, {} ).throwIfUndefined() as CzmlCatalogItem; await czmlItem.loadMapItems(); const czmlData = czmlItem.mapItems; expect(czmlData.length).toBeGreaterThan(0); const czmlFeature = czmlData[0].entities.values[0]; renderWithContexts( {}} />, viewState ); expect(screen.getByText(/ABC/)).toBeInTheDocument(); expect(screen.getByText(/2010/)).toBeInTheDocument(); expect(screen.getByText(/14\.4/)).toBeInTheDocument(); expect(screen.getByText(/2012/)).toBeInTheDocument(); expect(screen.getByText(/10\.7/)).toBeInTheDocument(); }); it("uses and completes a time-varying, string-form featureInfoTemplate", async function () { const json = await import("../../wwwroot/test/init/czml-with-template-1.json"); const czmlItem = upsertModelFromJson( CatalogMemberFactory, terria, "", "definition", json, {} ).throwIfUndefined() as CzmlCatalogItem; await czmlItem.loadMapItems(); const czmlData = czmlItem.mapItems; expect(czmlData.length).toBeGreaterThan(0); const czmlFeature = czmlData[0].entities.values[0]; czmlItem.setTrait(CommonStrata.user, "currentTime", "2010-02-02"); renderWithContexts( {}} />, viewState ); expect(screen.queryByText(/ABC/)).not.toBeInTheDocument(); expect(screen.queryByText(/DEF/)).not.toBeInTheDocument(); cleanup(); czmlItem.setTrait(CommonStrata.user, "currentTime", "2012-02-02"); renderWithContexts( {}} />, viewState ); expect(screen.getByText(/ABC/)).toBeInTheDocument(); expect(screen.queryByText(/DEF/)).not.toBeInTheDocument(); cleanup(); czmlItem.setTrait(CommonStrata.user, "currentTime", "2014-02-02"); renderWithContexts( {}} />, viewState ); expect(screen.queryByText(/ABC/)).not.toBeInTheDocument(); expect(screen.getByText(/DEF/)).toBeInTheDocument(); }); }); describe("feature info panel buttons", function () { it("renders buttons added using FeatureInfoPanel.addFeatureButton", function () { FeatureInfoPanel.addFeatureButton(viewState, ({ feature, item }) => { if (!(item instanceof TestModel)) { return; } const materialUsed = feature.properties?.getValue(JulianDate.now())[ "material" ]; return materialUsed ? { text: `More info on ${materialUsed}`, title: "Show more info on material used", onClick() {} } : undefined; }); renderWithContexts( {}} />, viewState ); expect(screen.getByText("More info on steel")).toBeInTheDocument(); }); }); }); class TestModelTraits extends mixTraits( FeatureInfoUrlTemplateTraits, MappableTraits, DiscretelyTimeVaryingTraits ) {} class TestModel extends MappableMixin( DiscretelyTimeVaryingMixin(CatalogMemberMixin(CreateModel(TestModelTraits))) ) { constructor(...args: ModelConstructorParameters) { super(...args); makeObservable(this); } get mapItems(): MapItem[] { throw new Error("Method not implemented."); } protected forceLoadMapItems(): Promise { throw new Error("Method not implemented."); } @observable _discreteTimes: string[] = []; get discreteTimes() { return this._discreteTimes.map((t) => ({ time: t, tag: undefined })); } }