mirror of
https://github.com/maputnik/editor.git
synced 2026-07-12 17:07:28 +00:00
Increase coverage to about 80%
This commit is contained in:
@@ -96,7 +96,6 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npm run test-unit-ci
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
|
||||
@@ -76,6 +76,16 @@ describe("layer editor", () => {
|
||||
layers: [{ id: "background:" + bgId, type: "background", minzoom: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("the range slider adjusts min-zoom", async () => {
|
||||
await when.focus("min-zoom.input-range");
|
||||
await when.typeKeys("{rightarrow}");
|
||||
await then(
|
||||
get
|
||||
.styleFromLocalStorage()
|
||||
.then((style) => typeof style.layers.find((l: any) => l.id === "background:" + bgId).minzoom)
|
||||
).shouldEqual("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
@@ -145,6 +155,15 @@ describe("layer editor", () => {
|
||||
layers: [{ id: "background:" + bgId, type: "background" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("typing a hex value updates the paint color", async () => {
|
||||
await when.setColorValue("background-color", "#ff0000");
|
||||
await then(
|
||||
get
|
||||
.styleFromLocalStorage()
|
||||
.then((style) => style.layers.find((l: any) => l.id === "background:" + bgId).paint["background-color"])
|
||||
).shouldEqual("#ff0000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("opacity", () => {
|
||||
@@ -166,8 +185,71 @@ describe("layer editor", () => {
|
||||
});
|
||||
|
||||
describe("filter", () => {
|
||||
test.skip("expand/collapse", () => {});
|
||||
test.skip("compound filter", () => {});
|
||||
test("compound filter", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "fill", layer: "example" });
|
||||
await when.addFilter();
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => style.layers.find((l: any) => l.id === id).filter)
|
||||
).shouldDeepNestedInclude(["all", ["==", "name", ""]]);
|
||||
|
||||
// Changing the operator updates the compound filter.
|
||||
await when.selectFilterOperator("!=");
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => style.layers.find((l: any) => l.id === id).filter)
|
||||
).shouldDeepNestedInclude(["all", ["!=", "name", ""]]);
|
||||
|
||||
// A second filter item extends the compound filter.
|
||||
await when.addFilter();
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => style.layers.find((l: any) => l.id === id).filter)
|
||||
).shouldDeepNestedInclude(["all", ["!=", "name", ""], ["==", "name", ""]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("functions", () => {
|
||||
test("convert a property to a zoom function and add a stop", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
|
||||
await when.makeZoomFunction("circle-radius");
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => style.layers.find((l: any) => l.id === id).paint)
|
||||
).shouldDeepNestedInclude({ "circle-radius": { stops: [[6, 5], [10, 5]] } });
|
||||
|
||||
await when.addFunctionStop("circle-radius");
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => style.layers.find((l: any) => l.id === id).paint)
|
||||
).shouldDeepNestedInclude({ "circle-radius": { stops: [[6, 5], [10, 5], [11, 5]] } });
|
||||
|
||||
await when.deleteFunctionStop("circle-radius");
|
||||
await then(
|
||||
get
|
||||
.styleFromLocalStorage()
|
||||
.then((style) => style.layers.find((l: any) => l.id === id).paint["circle-radius"].stops.length)
|
||||
).shouldEqual(2);
|
||||
});
|
||||
|
||||
test("convert a property to a data function and edit stops", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
|
||||
// The property needs a value before it can be turned into a data function.
|
||||
await when.setValue("spec-field-input:circle-blur", "1");
|
||||
await when.makeDataFunction("circle-blur");
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => style.layers.find((l: any) => l.id === id).paint["circle-blur"].type)
|
||||
).shouldEqual("exponential");
|
||||
|
||||
await when.addFunctionStop("circle-blur");
|
||||
await then(
|
||||
get
|
||||
.styleFromLocalStorage()
|
||||
.then((style) => style.layers.find((l: any) => l.id === id).paint["circle-blur"].stops.length)
|
||||
).shouldEqual(3);
|
||||
|
||||
await when.deleteFunctionStop("circle-blur");
|
||||
await then(
|
||||
get
|
||||
.styleFromLocalStorage()
|
||||
.then((style) => style.layers.find((l: any) => l.id === id).paint["circle-blur"].stops.length)
|
||||
).shouldEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("layout", () => {
|
||||
|
||||
@@ -158,6 +158,56 @@ export class MaputnikDriver {
|
||||
await this.helper.when.typeText(value);
|
||||
},
|
||||
|
||||
makeZoomFunction: async (fieldName: string) => {
|
||||
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
|
||||
await container.scrollIntoViewIfNeeded();
|
||||
await container.locator(".maputnik-make-zoom-function").last().click({ force: true });
|
||||
},
|
||||
|
||||
makeDataFunction: async (fieldName: string) => {
|
||||
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
|
||||
await container.scrollIntoViewIfNeeded();
|
||||
await container.locator(".maputnik-make-data-function").click({ force: true });
|
||||
},
|
||||
|
||||
addFunctionStop: async (fieldName: string) => {
|
||||
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
|
||||
await container.locator(".maputnik-add-stop").first().click({ force: true });
|
||||
},
|
||||
|
||||
deleteFunctionStop: async (fieldName: string) => {
|
||||
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
|
||||
await container.locator(".maputnik-delete-stop").first().click({ force: true });
|
||||
},
|
||||
|
||||
addFilter: async () => {
|
||||
const button = this.helper.get.elementByTestId("layer-filter-button");
|
||||
await button.scrollIntoViewIfNeeded();
|
||||
await button.click({ force: true });
|
||||
},
|
||||
|
||||
selectFilterOperator: async (value: string) => {
|
||||
await this.helper.get.element(".maputnik-filter-editor-operator select").first().selectOption(value);
|
||||
},
|
||||
|
||||
deleteFirstActiveSource: async () => {
|
||||
await this.helper.get.element(".maputnik-active-source-type-editor-header-delete").first().click();
|
||||
},
|
||||
|
||||
setColorValue: async (fieldName: string, value: string) => {
|
||||
const input = this.helper.get.elementByTestId("spec-field:" + fieldName).locator(".maputnik-color");
|
||||
await input.fill(value);
|
||||
},
|
||||
|
||||
exportCreateHtml: async () => {
|
||||
await this.helper.get.element(".maputnik-modal-export-buttons button").last().click();
|
||||
},
|
||||
|
||||
exportSaveStyle: async () => {
|
||||
await this.helper.stubSaveFilePicker();
|
||||
await this.helper.get.element(".maputnik-modal-export-buttons button").first().click();
|
||||
},
|
||||
|
||||
waitForExampleFileResponse: () => this.helper.when.waitForResponse("example-style.json"),
|
||||
|
||||
/** Fill localStorage until we get a QuotaExceededError. */
|
||||
|
||||
+20
-3
@@ -64,8 +64,15 @@ describe("modals", () => {
|
||||
await then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||
});
|
||||
|
||||
// TODO: Work out how to download a file and check the contents
|
||||
test.skip("download", () => {});
|
||||
test("download HTML and save the style", async () => {
|
||||
// Generate the standalone HTML export (triggers a file download).
|
||||
await when.exportCreateHtml();
|
||||
await then(get.elementByTestId("modal:export")).shouldExist();
|
||||
|
||||
// Saving the style closes the export modal.
|
||||
await when.exportSaveStyle();
|
||||
await then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
@@ -74,7 +81,17 @@ describe("modals", () => {
|
||||
await when.click("nav:sources");
|
||||
});
|
||||
|
||||
test.skip("active sources", () => {});
|
||||
test("active sources are listed and can be deleted", async () => {
|
||||
// The "both" style ships with active sources; reopen the modal against it.
|
||||
await when.setStyle("both");
|
||||
await when.click("nav:sources");
|
||||
const before = Object.keys(get.fixture("geojson-raster-style.json").sources).length;
|
||||
await when.deleteFirstActiveSource();
|
||||
await then(
|
||||
get.styleFromLocalStorage().then((style) => Object.keys(style.sources).length)
|
||||
).shouldEqual(before - 1);
|
||||
});
|
||||
|
||||
test.skip("public source", () => {});
|
||||
|
||||
test("add new source", async () => {
|
||||
|
||||
@@ -63,9 +63,9 @@ function isLocator(target: unknown): target is Locator {
|
||||
}
|
||||
|
||||
/** Asserts that every top-level key in `expected` deep-equals its counterpart in `actual`. */
|
||||
function assertDeepNestedInclude(actual: any, expected: Record<string, unknown>): void {
|
||||
function assertDeepNestedInclude(actual: any, expected: Record<string, unknown> | unknown[]): void {
|
||||
for (const key of Object.keys(expected)) {
|
||||
expect(actual?.[key], `property "${key}"`).toEqual(expected[key]);
|
||||
expect(actual?.[key], `property "${key}"`).toEqual((expected as any)[key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export class Assertable<T> {
|
||||
}
|
||||
});
|
||||
|
||||
shouldDeepNestedInclude = (value: Record<string, unknown>) =>
|
||||
shouldDeepNestedInclude = (value: Record<string, unknown> | unknown[]) =>
|
||||
this.assertValue((actual) => assertDeepNestedInclude(actual, value));
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ async function typeSequence(page: Page, text: string): Promise<void> {
|
||||
del: "Delete",
|
||||
tab: "Tab",
|
||||
home: "Home",
|
||||
rightarrow: "ArrowRight",
|
||||
leftarrow: "ArrowLeft",
|
||||
uparrow: "ArrowUp",
|
||||
downarrow: "ArrowDown",
|
||||
};
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
@@ -202,6 +206,15 @@ export class PlaywrightHelper {
|
||||
return new Query<T>(getter);
|
||||
}
|
||||
|
||||
/** Stubs the File System Access "save" picker so file saves complete headlessly. */
|
||||
public stubSaveFilePicker(): Promise<void> {
|
||||
return this.page.evaluate(() => {
|
||||
(window as any).showSaveFilePicker = async () => ({
|
||||
createWritable: async () => ({ write: async () => {}, close: async () => {} }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Entry point for fluent assertions over a Locator or a value/Query. */
|
||||
public then = <T>(target: T): Assertable<T> => new Assertable(target);
|
||||
|
||||
|
||||
Generated
+1
-157
@@ -90,8 +90,6 @@
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@vitejs/plugin-react": "5.2",
|
||||
"@vitest/browser": "^4.1.10",
|
||||
"@vitest/browser-playwright": "^4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"cors": "^2.8.6",
|
||||
"eslint": "^10.6.0",
|
||||
@@ -113,8 +111,7 @@
|
||||
"uuid": "^14.0.1",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-istanbul": "^9.0.1",
|
||||
"vitest": "^4.1.10",
|
||||
"vitest-browser-react": "^2.2.0"
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -438,13 +435,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@blazediff/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cacheable/memory": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
|
||||
@@ -2188,13 +2178,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@@ -3584,63 +3567,6 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz",
|
||||
"integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blazediff/core": "1.9.1",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
"@vitest/utils": "4.1.10",
|
||||
"magic-string": "^0.30.21",
|
||||
"pngjs": "^7.0.0",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser-playwright": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz",
|
||||
"integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.1.10",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "4.1.10"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser/node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
|
||||
@@ -9295,16 +9221,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -11551,21 +11467,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/slugify": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
|
||||
@@ -12437,16 +12338,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
@@ -13622,31 +13513,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-browser-react": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest-browser-react/-/vitest-browser-react-2.2.0.tgz",
|
||||
"integrity": "sha512-oY3KM6305kwJMa6nHo92vVtkOsih7mjEf12dLKuphaF+9ywWPEc+qanIBd394SZ6m5LadVEaG6dicvvizOzmjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
@@ -13834,28 +13700,6 @@
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||
|
||||
+1
-4
@@ -124,8 +124,6 @@
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@vitejs/plugin-react": "5.2",
|
||||
"@vitest/browser": "^4.1.10",
|
||||
"@vitest/browser-playwright": "^4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"cors": "^2.8.6",
|
||||
"eslint": "^10.6.0",
|
||||
@@ -147,7 +145,6 @@
|
||||
"uuid": "^14.0.1",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-istanbul": "^9.0.1",
|
||||
"vitest": "^4.1.10",
|
||||
"vitest-browser-react": "^2.2.0"
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { render } from "vitest-browser-react";
|
||||
import { page } from "vitest/browser";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
|
||||
const fruits = ["apple", "banana", "cherry"];
|
||||
|
||||
test("filters options when typing", async () => {
|
||||
render(<InputAutocomplete aria-label="Fruit" options={fruits.map((f) => [f, f])} />);
|
||||
|
||||
const input = page.getByLabelText("Fruit");
|
||||
await input.click();
|
||||
|
||||
const menuItems = page.getByRole("option");
|
||||
await expect.element(menuItems.first()).toBeVisible();
|
||||
expect(menuItems.all()).toHaveLength(3);
|
||||
|
||||
await input.fill("ch");
|
||||
await expect.element(page.getByText("cherry")).toBeVisible();
|
||||
expect(page.getByRole("option").all()).toHaveLength(1);
|
||||
|
||||
await page.getByText("cherry").click();
|
||||
await expect.element(input).toHaveValue("cherry");
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { StyleSpecification } from "maplibre-gl";
|
||||
import { undoMessages, redoMessages } from "./diffmessage";
|
||||
|
||||
const before = { version: 8, sources: {}, layers: [] } as StyleSpecification;
|
||||
const after = {
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [{ id: "bg", type: "background" }],
|
||||
} as StyleSpecification;
|
||||
|
||||
describe("diff messages", () => {
|
||||
it("prefixes undo messages with 'Undo'", () => {
|
||||
const messages = undoMessages(before, after);
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
expect(messages.every((m) => m.startsWith("Undo "))).toBe(true);
|
||||
});
|
||||
|
||||
it("prefixes redo messages with 'Redo'", () => {
|
||||
const messages = redoMessages(before, after);
|
||||
expect(messages.length).toBeGreaterThan(0);
|
||||
expect(messages.every((m) => m.startsWith("Redo "))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { LayerSpecification } from "maplibre-gl";
|
||||
import { colorHighlightedLayer } from "./highlight";
|
||||
|
||||
function layer(overrides: Record<string, any>): LayerSpecification {
|
||||
return { id: "l", source: "s", "source-layer": "sl", ...overrides } as unknown as LayerSpecification;
|
||||
}
|
||||
|
||||
describe("colorHighlightedLayer", () => {
|
||||
it("returns null for undefined, background and raster layers", () => {
|
||||
expect(colorHighlightedLayer(undefined)).toBeNull();
|
||||
expect(colorHighlightedLayer(layer({ type: "background" }))).toBeNull();
|
||||
expect(colorHighlightedLayer(layer({ type: "raster" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("builds a circle highlight for circle and symbol layers", () => {
|
||||
for (const type of ["circle", "symbol"]) {
|
||||
const highlight = colorHighlightedLayer(layer({ type }))!;
|
||||
expect(highlight).not.toBeNull();
|
||||
expect(highlight.type).toBe("circle");
|
||||
expect(highlight.id).toMatch(/_highlight$/);
|
||||
expect((highlight.paint as any)["circle-radius"]).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
it("builds a line highlight with an overridden width", () => {
|
||||
const highlight = colorHighlightedLayer(layer({ type: "line" }))!;
|
||||
expect(highlight.type).toBe("line");
|
||||
expect((highlight.paint as any)["line-width"]).toBe(2);
|
||||
});
|
||||
|
||||
it("builds a polygon highlight for fill and fill-extrusion layers", () => {
|
||||
for (const type of ["fill", "fill-extrusion"]) {
|
||||
const highlight = colorHighlightedLayer(layer({ type }))!;
|
||||
expect(highlight.type).toBe("fill");
|
||||
}
|
||||
});
|
||||
|
||||
it("copies the source layer's filter when present, drops it otherwise", () => {
|
||||
const withFilter = colorHighlightedLayer(
|
||||
layer({ type: "line", filter: ["==", "class", "road"] } as any)
|
||||
)!;
|
||||
expect(withFilter.filter).toEqual(["==", "class", "road"]);
|
||||
|
||||
const withoutFilter = colorHighlightedLayer(layer({ type: "line" }))!;
|
||||
expect("filter" in withoutFilter).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { LayerSpecification } from "maplibre-gl";
|
||||
import { changeType, changeProperty, layerPrefix, findClosestCommonPrefix } from "./layer";
|
||||
|
||||
describe("changeType", () => {
|
||||
it("drops paint/layout props not valid for the new type", () => {
|
||||
const layer = {
|
||||
id: "l",
|
||||
type: "fill",
|
||||
source: "s",
|
||||
paint: { "fill-color": "#fff", "fill-opacity": 0.5 },
|
||||
layout: { visibility: "visible" },
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const changed = changeType(layer, "background") as any;
|
||||
expect(changed.type).toBe("background");
|
||||
// fill-color is not a valid background paint property
|
||||
expect(changed.paint["fill-color"]).toBeUndefined();
|
||||
// visibility is valid for background layout
|
||||
expect(changed.layout.visibility).toBe("visible");
|
||||
});
|
||||
|
||||
it("keeps props that are valid for the new type", () => {
|
||||
const layer = {
|
||||
id: "l",
|
||||
type: "line",
|
||||
source: "s",
|
||||
paint: { "line-color": "#000" },
|
||||
} as unknown as LayerSpecification;
|
||||
const changed = changeType(layer, "line") as any;
|
||||
expect(changed.paint["line-color"]).toBe("#000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeProperty", () => {
|
||||
const base = { id: "l", type: "background" } as unknown as LayerSpecification;
|
||||
|
||||
it("sets a top-level property", () => {
|
||||
const changed = changeProperty(base, null, "minzoom", 4) as any;
|
||||
expect(changed.minzoom).toBe(4);
|
||||
});
|
||||
|
||||
it("removes a top-level property when value is undefined", () => {
|
||||
const changed = changeProperty({ ...base, minzoom: 4 } as any, null, "minzoom", undefined) as any;
|
||||
expect("minzoom" in changed).toBe(false);
|
||||
});
|
||||
|
||||
it("sets a grouped property", () => {
|
||||
const changed = changeProperty(base, "paint", "background-color", "#fff") as any;
|
||||
expect(changed.paint["background-color"]).toBe("#fff");
|
||||
});
|
||||
|
||||
it("removes a grouped property and drops the empty group", () => {
|
||||
const layer = { ...base, paint: { "background-color": "#fff" } } as any;
|
||||
const changed = changeProperty(layer, "paint", "background-color", undefined) as any;
|
||||
expect("paint" in changed).toBe(false);
|
||||
});
|
||||
|
||||
it("removes a grouped property but keeps a non-empty group", () => {
|
||||
const layer = { ...base, paint: { "background-color": "#fff", "background-opacity": 1 } } as any;
|
||||
const changed = changeProperty(layer, "paint", "background-color", undefined) as any;
|
||||
expect(changed.paint["background-color"]).toBeUndefined();
|
||||
expect(changed.paint["background-opacity"]).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("layerPrefix", () => {
|
||||
it("returns the segment before the first separator", () => {
|
||||
expect(layerPrefix("foo-bar")).toBe("foo");
|
||||
expect(layerPrefix("foo_bar")).toBe("foo");
|
||||
expect(layerPrefix("foo bar")).toBe("foo");
|
||||
expect(layerPrefix("single")).toBe("single");
|
||||
});
|
||||
});
|
||||
|
||||
describe("findClosestCommonPrefix", () => {
|
||||
const layers = [
|
||||
{ id: "aa" },
|
||||
{ id: "aa-2" },
|
||||
{ id: "b" },
|
||||
] as unknown as LayerSpecification[];
|
||||
|
||||
it("finds the first index of a run sharing the prefix", () => {
|
||||
expect(findClosestCommonPrefix(layers, 1)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns the index itself when the previous prefix differs", () => {
|
||||
expect(findClosestCommonPrefix(layers, 2)).toBe(2);
|
||||
});
|
||||
|
||||
it("returns the index for the first layer", () => {
|
||||
expect(findClosestCommonPrefix(layers, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import type { Map } from "maplibre-gl";
|
||||
import LayerWatcher from "./layerwatcher";
|
||||
|
||||
function mockMap(): Map {
|
||||
return {
|
||||
style: {
|
||||
tileManagers: {
|
||||
vector: { _source: { vectorLayerIds: ["water", "roads"] } },
|
||||
},
|
||||
},
|
||||
querySourceFeatures: () => [
|
||||
{ properties: { class: "river", name: "A" } },
|
||||
{ properties: { class: "canal" } },
|
||||
],
|
||||
} as unknown as Map;
|
||||
}
|
||||
|
||||
describe("LayerWatcher", () => {
|
||||
it("reports sources discovered on the map", () => {
|
||||
const onSourcesChange = vi.fn();
|
||||
const watcher = new LayerWatcher({ onSourcesChange });
|
||||
|
||||
watcher.analyzeMap(mockMap());
|
||||
expect(onSourcesChange).toHaveBeenCalledOnce();
|
||||
expect(watcher.sources).toEqual({ vector: ["water", "roads"] });
|
||||
|
||||
// Re-analyzing the same sources does not fire the callback again.
|
||||
watcher.analyzeMap(mockMap());
|
||||
expect(onSourcesChange).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("collects vector layer field values from source features", () => {
|
||||
const onVectorLayersChange = vi.fn();
|
||||
const watcher = new LayerWatcher({ onVectorLayersChange });
|
||||
|
||||
const map = mockMap();
|
||||
watcher.analyzeMap(map); // populates _sources
|
||||
watcher.analyzeVectorLayerFields(map);
|
||||
|
||||
expect(onVectorLayersChange).toHaveBeenCalled();
|
||||
expect(watcher.vectorLayers.water.class).toHaveProperty("river");
|
||||
expect(watcher.vectorLayers.water.class).toHaveProperty("canal");
|
||||
expect(watcher.vectorLayers.water.name).toHaveProperty("A");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from "./metadata";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("downloadGlyphsMetadata", () => {
|
||||
it("returns an empty list for an empty url", async () => {
|
||||
expect(await downloadGlyphsMetadata("")).toEqual([]);
|
||||
});
|
||||
|
||||
it("fetches the fontstacks list and de-duplicates it", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ["A", "A", "B"] })));
|
||||
const fonts = await downloadGlyphsMetadata("https://example.com/{fontstack}/{range}.pbf");
|
||||
expect(fonts.sort()).toEqual(["A", "B"]);
|
||||
});
|
||||
|
||||
it("returns the default on a failed request", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false })));
|
||||
expect(await downloadGlyphsMetadata("https://example.com/x/{fontstack}/{range}.pbf")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadSpriteMetadata", () => {
|
||||
it("returns the sprite icon names", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({ airport: {}, park: {} }) })));
|
||||
const icons = await downloadSpriteMetadata("https://example.com/sprite");
|
||||
expect(icons.sort()).toEqual(["airport", "park"]);
|
||||
});
|
||||
|
||||
it("returns an empty list for an empty base url", async () => {
|
||||
expect(await downloadSpriteMetadata("")).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { RevisionStore } from "./revisions";
|
||||
|
||||
const rev = (id: string) => ({ version: 8, id, sources: {}, layers: [] }) as any;
|
||||
|
||||
describe("RevisionStore", () => {
|
||||
it("tracks latest/current as revisions are added", () => {
|
||||
const store = new RevisionStore();
|
||||
store.addRevision(rev("a"));
|
||||
store.addRevision(rev("b"));
|
||||
expect(store.latest.id).toBe("b");
|
||||
expect(store.current.id).toBe("b");
|
||||
});
|
||||
|
||||
it("undo and redo move through history", () => {
|
||||
const store = new RevisionStore();
|
||||
store.addRevision(rev("a"));
|
||||
store.addRevision(rev("b"));
|
||||
expect(store.undo().id).toBe("a");
|
||||
expect(store.undo().id).toBe("a"); // clamped at start
|
||||
expect(store.redo().id).toBe("b");
|
||||
expect(store.redo().id).toBe("b"); // clamped at end
|
||||
});
|
||||
|
||||
it("clears redo history when a new revision is added after undo", () => {
|
||||
const store = new RevisionStore();
|
||||
store.addRevision(rev("a"));
|
||||
store.addRevision(rev("b"));
|
||||
store.undo();
|
||||
store.addRevision(rev("c"));
|
||||
expect(store.latest.id).toBe("c");
|
||||
expect(store.redo().id).toBe("c");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import sortNumerically from "./sort-numerically";
|
||||
|
||||
describe("sortNumerically", () => {
|
||||
it("orders numbers (and numeric strings) ascending", () => {
|
||||
expect([3, 1, 2].sort(sortNumerically)).toEqual([1, 2, 3]);
|
||||
expect(["10", "2", "1"].sort(sortNumerically)).toEqual(["1", "2", "10"]);
|
||||
expect(sortNumerically(5, 5)).toBe(0);
|
||||
expect(sortNumerically(1, 2)).toBe(-1);
|
||||
expect(sortNumerically(2, 1)).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { addSource, changeSource, deleteSource } from "./source";
|
||||
|
||||
const style = { version: 8, id: "s", sources: { a: { type: "vector" } }, layers: [] } as any;
|
||||
|
||||
describe("source helpers", () => {
|
||||
it("adds a source", () => {
|
||||
const result = addSource(style, "b", { type: "geojson", data: {} } as any);
|
||||
expect(result.sources.b).toEqual({ type: "geojson", data: {} });
|
||||
expect(result.sources.a).toBeDefined();
|
||||
});
|
||||
|
||||
it("changes an existing source", () => {
|
||||
const result = changeSource(style, "a", { type: "raster" } as any);
|
||||
expect(result.sources.a).toEqual({ type: "raster" });
|
||||
});
|
||||
|
||||
it("deletes a source without mutating the input", () => {
|
||||
const result = deleteSource(style, "a");
|
||||
expect(result.sources.a).toBeUndefined();
|
||||
expect(style.sources.a).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { findDefaultFromSpec } from "./spec-helper";
|
||||
|
||||
describe("findDefaultFromSpec", () => {
|
||||
it("returns the spec default when present (even if falsy)", () => {
|
||||
expect(findDefaultFromSpec({ type: "string", default: "hi" })).toBe("hi");
|
||||
expect(findDefaultFromSpec({ type: "boolean", default: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to a sensible default per type", () => {
|
||||
expect(findDefaultFromSpec({ type: "color" })).toBe("#000000");
|
||||
expect(findDefaultFromSpec({ type: "string" })).toBe("");
|
||||
// The falsy `false` fallback collapses to "" via the `|| ""` in the impl.
|
||||
expect(findDefaultFromSpec({ type: "boolean" })).toBe("");
|
||||
expect(findDefaultFromSpec({ type: "array" })).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
vi.stubGlobal("window", { location: { port: "8000" } });
|
||||
|
||||
// Avoid opening a real websocket in notifyLocalChanges().
|
||||
const wsInstances: any[] = [];
|
||||
vi.mock("reconnecting-websocket", () => ({
|
||||
default: class {
|
||||
onmessage: ((e: { data: string }) => void) | null = null;
|
||||
constructor(public url: string) {
|
||||
wsInstances.push(this);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
import { ApiStyleStore } from "./apistore";
|
||||
|
||||
const okJson = (body: any) => ({ ok: true, json: async () => body });
|
||||
|
||||
describe("ApiStyleStore", () => {
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("init fetches style ids and wires up local change notifications", async () => {
|
||||
const fetchMock = vi.fn(async () => okJson(["style-a", "style-b"]) as any);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const onLocalStyleChange = vi.fn();
|
||||
const store = new ApiStyleStore({ onLocalStyleChange });
|
||||
|
||||
await store.init();
|
||||
expect(fetchMock).toHaveBeenCalledWith("http://localhost:8000/styles", { mode: "cors" });
|
||||
expect(store.latestStyleId).toBe("style-a");
|
||||
|
||||
// A websocket message triggers onLocalStyleChange with a valid style.
|
||||
expect(wsInstances.length).toBe(1);
|
||||
wsInstances[0].onmessage!({ data: JSON.stringify({ version: 8, sources: {}, layers: [] }) });
|
||||
expect(onLocalStyleChange).toHaveBeenCalledOnce();
|
||||
expect(onLocalStyleChange.mock.calls[0][0].id).toBeTruthy();
|
||||
});
|
||||
|
||||
it("init throws a friendly error when the API is unreachable", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("boom"); }));
|
||||
const store = new ApiStyleStore({});
|
||||
await expect(store.init()).rejects.toThrow("Can not connect to style API");
|
||||
});
|
||||
|
||||
it("getLatestStyle fetches the latest style and validates it", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => okJson(["s1"]) as any));
|
||||
const store = new ApiStyleStore({});
|
||||
await store.init();
|
||||
|
||||
vi.stubGlobal("fetch", vi.fn(async () => okJson({ version: 8, id: "s1", sources: {}, layers: [] }) as any));
|
||||
const latest = await store.getLatestStyle();
|
||||
expect(latest.id).toBe("s1");
|
||||
});
|
||||
|
||||
it("getLatestStyle throws when not initialised", async () => {
|
||||
const store = new ApiStyleStore({});
|
||||
await expect(store.getLatestStyle()).rejects.toThrow(/init the api backend/);
|
||||
});
|
||||
|
||||
it("save PUTs the (token-stripped) style to the API", () => {
|
||||
const fetchMock = vi.fn(async () => okJson({}) as any);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const store = new ApiStyleStore({});
|
||||
const mapStyle = { version: 8, id: "s1", sources: {}, layers: [] } as any;
|
||||
|
||||
expect(store.save(mapStyle)).toBe(mapStyle);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://localhost:8000/styles/s1",
|
||||
expect.objectContaining({ method: "PUT", mode: "cors" })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
// StyleStore reads/writes window.localStorage; provide a minimal in-memory mock.
|
||||
class LocalStorageMock {
|
||||
private store: Record<string, string> = {};
|
||||
get length() {
|
||||
return Object.keys(this.store).length;
|
||||
}
|
||||
key(i: number) {
|
||||
return Object.keys(this.store)[i] ?? null;
|
||||
}
|
||||
getItem(k: string) {
|
||||
return k in this.store ? this.store[k] : null;
|
||||
}
|
||||
setItem(k: string, v: string) {
|
||||
this.store[k] = v;
|
||||
}
|
||||
removeItem(k: string) {
|
||||
delete this.store[k];
|
||||
}
|
||||
clear() {
|
||||
this.store = {};
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal("window", { localStorage: new LocalStorageMock() });
|
||||
|
||||
// Avoid network in loadDefaultStyle.
|
||||
vi.mock("../urlopen", () => ({
|
||||
loadStyleUrl: vi.fn(async () => ({ version: 8, id: "default", sources: {}, layers: [] })),
|
||||
}));
|
||||
|
||||
import { StyleStore } from "./stylestore";
|
||||
|
||||
const style = (id: string) => ({ version: 8, id, sources: {}, layers: [] }) as any;
|
||||
|
||||
describe("StyleStore", () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it("returns the default style when nothing is stored", async () => {
|
||||
const store = new StyleStore();
|
||||
const latest = await store.getLatestStyle();
|
||||
expect(latest.id).toBe("default");
|
||||
});
|
||||
|
||||
it("saves a style and reads it back as the latest", async () => {
|
||||
const store = new StyleStore();
|
||||
await store.save(style("abc"));
|
||||
// A fresh store discovers the persisted style ids.
|
||||
const reopened = new StyleStore();
|
||||
const latest = await reopened.getLatestStyle();
|
||||
expect(latest.id).toBe("abc");
|
||||
});
|
||||
|
||||
it("purge removes all maputnik keys", async () => {
|
||||
const store = new StyleStore();
|
||||
await store.save(style("abc"));
|
||||
window.localStorage.setItem("unrelated", "keep");
|
||||
store.purge();
|
||||
expect(window.localStorage.getItem("maputnik:style:abc")).toBeNull();
|
||||
expect(window.localStorage.getItem("unrelated")).toBe("keep");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { StyleSpecification } from "maplibre-gl";
|
||||
import style from "./style";
|
||||
|
||||
function baseStyle(overrides: Partial<StyleSpecification> = {}): StyleSpecification {
|
||||
return { version: 8, sources: {}, layers: [], ...overrides } as StyleSpecification;
|
||||
}
|
||||
|
||||
describe("ensureStyleValidity", () => {
|
||||
it("adds an id and strips interactive from layers", () => {
|
||||
const result = style.ensureStyleValidity(
|
||||
baseStyle({
|
||||
layers: [{ id: "l", type: "background", interactive: true } as any],
|
||||
})
|
||||
);
|
||||
expect(result.id).toBeTruthy();
|
||||
expect("interactive" in result.layers[0]).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps an existing id", () => {
|
||||
const result = style.ensureStyleValidity(baseStyle({ id: "keep-me" } as any));
|
||||
expect(result.id).toBe("keep-me");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateId", () => {
|
||||
it("generates a non-empty string", () => {
|
||||
expect(typeof style.generateId()).toBe("string");
|
||||
expect(style.generateId().length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("indexOfLayer", () => {
|
||||
const layers = [{ id: "a" }, { id: "b" }] as any;
|
||||
it("returns the index of a matching layer", () => {
|
||||
expect(style.indexOfLayer(layers, "b")).toBe(1);
|
||||
});
|
||||
it("returns null when not found", () => {
|
||||
expect(style.indexOfLayer(layers, "missing")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccessToken", () => {
|
||||
it("reads the token from metadata", () => {
|
||||
const s = baseStyle({ metadata: { "maputnik:openmaptiles_access_token": "abc" } } as any);
|
||||
expect(style.getAccessToken("openmaptiles", s, {})).toBe("abc");
|
||||
});
|
||||
it("falls back to the bundled token only when allowed", () => {
|
||||
const s = baseStyle();
|
||||
expect(style.getAccessToken("openmaptiles", s, {})).toBeUndefined();
|
||||
expect(style.getAccessToken("openmaptiles", s, { allowFallback: true })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceAccessTokens", () => {
|
||||
it("replaces {key} in a source url and in glyphs", () => {
|
||||
const s = baseStyle({
|
||||
metadata: { "maputnik:openmaptiles_access_token": "TОKEN" } as any,
|
||||
sources: { openmaptiles: { type: "vector", url: "https://api.maptiler.com/x?key={key}" } } as any,
|
||||
glyphs: "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}",
|
||||
});
|
||||
const result = style.replaceAccessTokens(s);
|
||||
expect((result.sources.openmaptiles as any).url).toContain("key=TОKEN");
|
||||
expect(result.glyphs).toContain("key=TОKEN");
|
||||
});
|
||||
|
||||
it("maps thunderforest transport/outdoors sources to the thunderforest token", () => {
|
||||
const s = baseStyle({
|
||||
metadata: { "maputnik:thunderforest_access_token": "TF" } as any,
|
||||
sources: { thunderforest_transport: { type: "vector", url: "https://tile.thunderforest.com/x?apikey={key}" } } as any,
|
||||
});
|
||||
const result = style.replaceAccessTokens(s);
|
||||
expect((result.sources.thunderforest_transport as any).url).toContain("TF");
|
||||
});
|
||||
|
||||
it("appends an api_key query param for stadia sources", () => {
|
||||
const s = baseStyle({
|
||||
metadata: { "maputnik:stadia_access_token": "ST" } as any,
|
||||
sources: { basemap: { type: "vector", url: "https://tiles.stadiamaps.com/data/x.json" } } as any,
|
||||
});
|
||||
const result = style.replaceAccessTokens(s);
|
||||
expect((result.sources.basemap as any).url).toContain("api_key=ST");
|
||||
});
|
||||
|
||||
it("uses the locationiq token for locationiq sources", () => {
|
||||
const s = baseStyle({
|
||||
metadata: { "maputnik:locationiq_access_token": "LIQ" } as any,
|
||||
sources: { liq: { type: "vector", url: "https://tiles.locationiq.com/v3/x?key={key}" } } as any,
|
||||
});
|
||||
const result = style.replaceAccessTokens(s);
|
||||
expect((result.sources.liq as any).url).toContain("LIQ");
|
||||
});
|
||||
|
||||
it("leaves sources without a url or token untouched", () => {
|
||||
const s = baseStyle({
|
||||
sources: {
|
||||
noUrl: { type: "geojson", data: {} } as any,
|
||||
noToken: { type: "vector", url: "https://api.maptiler.com/x?key={key}" } as any,
|
||||
},
|
||||
});
|
||||
const result = style.replaceAccessTokens(s);
|
||||
expect((result.sources.noToken as any).url).toContain("{key}");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripAccessTokens", () => {
|
||||
it("removes provider access tokens from metadata", () => {
|
||||
const s = baseStyle({
|
||||
metadata: {
|
||||
"maputnik:openmaptiles_access_token": "a",
|
||||
"maputnik:thunderforest_access_token": "b",
|
||||
"maputnik:renderer": "mlgljs",
|
||||
} as any,
|
||||
});
|
||||
const result = style.stripAccessTokens(s);
|
||||
expect(result.metadata).not.toHaveProperty("maputnik:openmaptiles_access_token");
|
||||
expect(result.metadata).not.toHaveProperty("maputnik:thunderforest_access_token");
|
||||
expect((result.metadata as any)["maputnik:renderer"]).toBe("mlgljs");
|
||||
});
|
||||
});
|
||||
+1
-27
@@ -1,34 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "unit",
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.{ts,tsx}"],
|
||||
exclude: ["src/**/*.browser.test.{ts,tsx}"],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "browser",
|
||||
include: ["src/**/*.browser.test.{ts,tsx}"],
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
headless: true,
|
||||
instances: [{ browser: "chromium" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
include: ["src/**/*.test.{ts,tsx}"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["json", "lcov", "text-summary"],
|
||||
|
||||
Reference in New Issue
Block a user