- {!bare ? (
+ {!bare && !isViewer ? (
diff --git a/frontend/src/utils/dimensionSignature.ts b/frontend/src/utils/dimensionSignature.ts
new file mode 100644
index 000000000..af7a7a6f2
--- /dev/null
+++ b/frontend/src/utils/dimensionSignature.ts
@@ -0,0 +1,71 @@
+import {
+ getResolvedScales,
+ translateUnitToNeuroglancer
+} from '@/omezarr-helper';
+
+import type { Metadata } from '@/omezarr-helper';
+
+export type DimensionSignature = {
+ axes: { name: string; unit: string }[];
+ scales: number[];
+};
+
+// ponytail: signature is axes (name+unit) + resolved voxel scales. That's the
+// "will these layers align in Neuroglancer" question; shape is deliberately
+// excluded per spec.
+export function getDimensionSignature(
+ metadata: Metadata
+): DimensionSignature | null {
+ const multiscale = metadata.multiscales?.[0];
+ if (!multiscale?.axes || multiscale.axes.length === 0) {
+ return null;
+ }
+ let scales: number[];
+ try {
+ scales = getResolvedScales(multiscale);
+ } catch {
+ return null;
+ }
+ if (scales.length === 0) {
+ return null;
+ }
+ return {
+ axes: multiscale.axes.map(axis => ({
+ name: axis.name.toLowerCase(),
+ unit: translateUnitToNeuroglancer(axis.unit as string) || ''
+ })),
+ scales
+ };
+}
+
+// Relative epsilon: voxel sizes span nm→µm, so absolute tolerance is wrong.
+// ponytail: 1e-3 relative; tighten if false matches appear.
+export function signaturesMatch(
+ a: DimensionSignature,
+ b: DimensionSignature,
+ epsilon = 1e-3
+): boolean {
+ if (a.axes.length !== b.axes.length) {
+ return false;
+ }
+ for (let i = 0; i < a.axes.length; i++) {
+ if (a.axes[i].name !== b.axes[i].name) {
+ return false;
+ }
+ if (a.axes[i].unit !== b.axes[i].unit) {
+ return false;
+ }
+ }
+ if (a.scales.length !== b.scales.length) {
+ return false;
+ }
+ for (let i = 0; i < a.scales.length; i++) {
+ const av = a.scales[i];
+ const bv = b.scales[i];
+ const denom = Math.max(Math.abs(av), Math.abs(bv), Number.MIN_VALUE);
+ if (Math.abs(av - bv) / denom > epsilon) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/frontend/ui-tests/tests/data-link-operations.spec.ts b/frontend/ui-tests/tests/data-link-operations.spec.ts
index 19bc717a4..7ecf90efe 100644
--- a/frontend/ui-tests/tests/data-link-operations.spec.ts
+++ b/frontend/ui-tests/tests/data-link-operations.spec.ts
@@ -25,7 +25,8 @@ const navigateToZarrDir = async (
const createLinkWithDirectoryNameOnlyFormat = async (page: any) => {
// Change data link format to "Directory name only" in the dialog
- const advancedSettingsAccordion = page.getByRole('button', {
+ const dialog = page.getByRole('dialog');
+ const advancedSettingsAccordion = dialog.getByRole('button', {
name: /advanced settings/i
});
await advancedSettingsAccordion.click();
@@ -33,7 +34,7 @@ const createLinkWithDirectoryNameOnlyFormat = async (page: any) => {
await nameOnlyInput.click();
await expect(nameOnlyInput).toBeChecked();
// Confirm in dialog
- const confirmButton = page.getByRole('button', {
+ const confirmButton = dialog.getByRole('button', {
name: /confirm|create|yes/i
});
await expect(confirmButton).toBeVisible({ timeout: 5000 });
@@ -64,11 +65,12 @@ test.describe('Data Link Operations', () => {
// Wait for zarr metadata to load
await expect(page.getByText('zarr.json')).toBeVisible();
+ const dialog = page.getByRole('dialog');
const dataLinkToggle = page.getByRole('checkbox', { name: /data link/i });
- const confirmButton = page.getByRole('button', {
+ const confirmButton = dialog.getByRole('button', {
name: /confirm|create|yes/i
});
- const confirmDeleteButton = page.getByRole('button', {
+ const confirmDeleteButton = dialog.getByRole('button', {
name: /delete/i
});
@@ -79,7 +81,7 @@ test.describe('Data Link Operations', () => {
await expect(confirmButton).toBeVisible();
// Check that the data link format is set to "Transparent path" in the dialog
- const advancedSettingsAccordion = page.getByRole('button', {
+ const advancedSettingsAccordion = dialog.getByRole('button', {
name: /advanced settings/i
});
await advancedSettingsAccordion.click();
diff --git a/frontend/ui-tests/tests/data-link-table-filtering.spec.ts b/frontend/ui-tests/tests/data-link-table-filtering.spec.ts
index b0b527945..7636a989f 100644
--- a/frontend/ui-tests/tests/data-link-table-filtering.spec.ts
+++ b/frontend/ui-tests/tests/data-link-table-filtering.spec.ts
@@ -111,7 +111,9 @@ test.describe('Data Link Table Filtering', () => {
// Create the data link
await dataLinkToggle.click();
- const confirmButton = page.getByRole('button', {
+ // Scope to the dialog: the zarr preview also has a "Create a view"
+ // button, so a page-wide /create/ match is ambiguous.
+ const confirmButton = page.getByRole('dialog').getByRole('button', {
name: /confirm|create|yes/i
});
await expect(confirmButton).toBeVisible({ timeout: 5000 });
diff --git a/frontend/ui-tests/tests/data-link-usage-tabs.spec.ts b/frontend/ui-tests/tests/data-link-usage-tabs.spec.ts
index 21251946a..1991ac0a9 100644
--- a/frontend/ui-tests/tests/data-link-usage-tabs.spec.ts
+++ b/frontend/ui-tests/tests/data-link-usage-tabs.spec.ts
@@ -29,8 +29,10 @@ async function openDataLinkUsageDialog(page: Page, dirName: string) {
await expect(dataLinkToggle).toBeVisible({ timeout: 10000 });
await dataLinkToggle.click();
- // Confirm data link creation in the dialog
- const confirmButton = page.getByRole('button', {
+ // Confirm data link creation in the dialog. Scope to the dialog: the zarr
+ // preview also has a "Create a view" button, so a page-wide /create/ match
+ // is ambiguous.
+ const confirmButton = page.getByRole('dialog').getByRole('button', {
name: /confirm|create|yes/i
});
await expect(confirmButton).toBeVisible({ timeout: 5000 });