Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cog viewer #26

Merged
merged 11 commits into from
Jan 28, 2025
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts


# playwright
test-results
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ To preview the app use:
yarn dev
```

This will start the app and make it available at <http://localhost:5173/>.
This will start the app and make it available at <http://localhost:3000/>.

## Configuring the Validation Form

Expand Down
25 changes: 25 additions & 0 deletions __mocks__/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,29 @@ export const handlers = [

return HttpResponse.json({ message: 'Data updated successfully' });
}),

http.get('/api/raster/cog/info', ({ request }) => {
return HttpResponse.json({
band_descriptions: [
['b1', 'Band 1'],
['b2', 'Band 2'],
['b3', 'Band 3'],
['b4', 'Band 4'],
],
});
}),

http.get('/api/raster/cog/WebMercatorQuad/tilejson.json', ({ request }) => {
return HttpResponse.json({
tilejson: "2.2.0",
tiles: [
"https://example.com/api/raster/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.png"
],
minzoom: 0,
maxzoom: 22,
bounds: [-180, -85.0511, 180, 85.0511],
center: [0, 0, 2]
}
)
}),
];
7 changes: 4 additions & 3 deletions __tests__/components/SuccessModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import { describe, it, vi, expect, afterEach } from 'vitest';
import SuccessModal from '@/components/SuccessModal';
import userEvent from '@testing-library/user-event';


// Mock StyledModal
Expand Down Expand Up @@ -60,7 +61,7 @@ describe('SuccessModal Component', () => {
).toBeInTheDocument();
});

it('calls setStatus with "idle" when OK is clicked', () => {
it('calls setStatus with "idle" when OK is clicked', async () => {
const mockSetStatus = vi.fn();
const props = {
type: 'edit' as const,
Expand All @@ -72,7 +73,7 @@ describe('SuccessModal Component', () => {

// Simulate clicking the OK button
const okButton = screen.getByText('OK');
fireEvent.click(okButton);
await userEvent.click(okButton);

// Verify setStatus is called
expect(mockSetStatus).toHaveBeenCalledWith('idle');
Expand Down
11 changes: 6 additions & 5 deletions __tests__/pages/EditIngestPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, beforeAll, afterEach, afterAll, vi } from 'vitest';
import { Amplify } from 'aws-amplify';
import { config } from '@/utils/aws-exports';
Expand Down Expand Up @@ -41,7 +42,7 @@ describe('Edit Ingest Page', () => {

// Simulate interaction to trigger the error
const pendingPullRequest = await screen.findByRole('button', { name: /seeded ingest #1/i });
fireEvent.click(pendingPullRequest);
await userEvent.click(pendingPullRequest);

// Verify ErrorModal appears
const errorModal = await screen.findByText(
Expand All @@ -59,7 +60,7 @@ describe('Edit Ingest Page', () => {

// Simulate interaction to open the form
const pendingPullRequest = await screen.findByRole('button', { name: /seeded ingest #1/i });
fireEvent.click(pendingPullRequest);
await userEvent.click(pendingPullRequest);

// Verify the form is displayed
await screen.findByLabelText('Collection');
Expand All @@ -68,11 +69,11 @@ describe('Edit Ingest Page', () => {
const descriptionInput = await screen.findByDisplayValue(/seeded ingest description #1/i );

// update something other than collection name
fireEvent.input(descriptionInput, { target: { value: 'updated description' } });
await userEvent.type(descriptionInput, 'updated description');

// Submit the form
const submitButton = await screen.findByRole('button', { name: /submit/i });
fireEvent.click(submitButton);
userEvent.click(submitButton);

// // Verify SuccessModal appears
await waitFor(() => {
Expand Down
181 changes: 181 additions & 0 deletions __tests__/playwright/COGControlsForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { expect, test } from '@/__tests__/playwright/setup-msw';
import { HttpResponse } from 'msw'

const testBands = ['R', 'G', "B"]
test.describe('COGControlsForm', () => {

for (const band of testBands) {
test(`band (${band}) selection enables update tile layer and updates parameters`, async ({ page }) => {
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// update tile layer button should be disabled by default
await expect(page.getByRole('button', {name: /update tile layer/i})).toBeDisabled()

// Wait for the Band dropdown to load
const bandRDropdown = page.locator(`[data-testid="band-${band}"] .ant-select-selector`);
await expect(bandRDropdown).toBeVisible();

// Open the dropdown and select "Band 4"
await bandRDropdown.click();
const bandOption = page.locator('.ant-select-item', { hasText: 'b4 - Band 4' });
await bandOption.click();

const requestPromise = page.waitForRequest((req) => req.url().includes('bidx=4') && req.method() === 'GET');

await page.getByRole('button', {name: /update tile layer/i}).click()

await requestPromise;
});
}

test('only render the band name, not the individual test band dropdowns for single band COG', async ({ page, http, worker }) => {
await worker.use(
http.get('/api/raster/cog/info', ({ request }) => {
return HttpResponse.json({
band_descriptions: [
['b1', 'Band 1'],
],
});
}),
)
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// update tile layer button should be disabled by default
await expect(page.getByRole('button', {name: /update tile layer/i})).toBeDisabled()

await expect(page.getByRole('heading', { name: 'Band: Band 1 (Index: 1)' })).toBeVisible()
const bandRDropdown = page.locator(`[data-testid="band-R"] .ant-select-selector`);
const bandGDropdown = page.locator(`[data-testid="band-G"] .ant-select-selector`);
const bandBDropdown = page.locator(`[data-testid="band-B"] .ant-select-selector`);
await expect(bandRDropdown).toBeHidden();
await expect(bandGDropdown).toBeHidden();
await expect(bandBDropdown).toBeHidden();
});


test('colormap selection enables update tile layer and updates parameters', async ({ page }) => {
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// update tile layer button should be disabled by default
await expect(page.getByRole('button', {name: /update tile layer/i})).toBeDisabled()

// Wait for the Band (R) dropdown to load
const colormapDropdown = page.locator('[data-testid="colormap"] .ant-select-selector');
await expect(colormapDropdown).toBeVisible();

// Open the dropdown and select "Band 2"
await colormapDropdown.click();
const colormapOption = page.locator('.ant-select-item', { hasText: 'CFastie' });
await colormapOption.click();

const requestPromise = page.waitForRequest((req) => req.url().includes('colormap_name=cfastie') && req.method() === 'GET');

await page.getByRole('button', {name: /update tile layer/i}).click()

await requestPromise;
});

test('colorformula entry enables update tile layer and updates parameters', async ({ page }) => {
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// update tile layer button should be disabled by default
await expect(page.getByRole('button', {name: /update tile layer/i})).toBeDisabled()

// Wait for the Band (R) dropdown to load
const colorFormulaInput = page.getByLabel(/color formula/i);
await expect(colorFormulaInput).toBeVisible();

await colorFormulaInput.fill('playwright')

const requestPromise = page.waitForRequest((req) => req.url().includes('color_formula=playwright') && req.method() === 'GET');

await page.getByRole('button', {name: /update tile layer/i}).click()

await requestPromise;
});

test('resampling selection enables update tile layer and updates parameters', async ({ page }) => {
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// update tile layer button should be disabled by default
await expect(page.getByRole('button', {name: /update tile layer/i})).toBeDisabled()

// Wait for the dropdown to load
const resamplingDropdown = page.locator('[data-testid="resampling"] .ant-select-selector');
await expect(resamplingDropdown).toBeVisible();

// Open the dropdown and select "Bilinear"
await resamplingDropdown.click();
const resamplingOption = page.locator('.ant-select-item', { hasText: 'Bilinear' });
await resamplingOption.click();

const requestPromise = page.waitForRequest((req) => req.url().includes('resampling=bilinear') && req.method() === 'GET');

await page.getByRole('button', {name: /update tile layer/i}).click()

await requestPromise;
});

test('nodata entry enables update tile layer and updates parameters', async ({ page }) => {
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// update tile layer button should be disabled by default
await expect(page.getByRole('button', {name: /update tile layer/i})).toBeDisabled()

// Wait for the Band (R) dropdown to load
const nodataInput = page.getByLabel(/nodata value/i);
await expect(nodataInput).toBeVisible();

await nodataInput.fill('255')

const requestPromise = page.waitForRequest((req) => req.url().includes('nodata=255') && req.method() === 'GET');

await page.getByRole('button', {name: /update tile layer/i}).click()

await requestPromise;
});

test('renders correctly with mock metadata', async ({ page }) => {
// Navigate to the page with COGControlsForm
await page.goto('/cog-viewer');

await page.getByPlaceholder(/Enter COG URL/i).fill('s3://test.com')
await page.getByRole('button', {name: /load/i}).click();

// Verify the initial state
await expect(page.locator('[data-testid="band-R"]')).toBeVisible();
await expect(page.locator('[data-testid="band-R"]')).toContainText('b1 - Band 1');

// Verify the initial state
await expect(page.locator('[data-testid="band-G"]')).toBeVisible();
await expect(page.locator('[data-testid="band-G"]')).toContainText('b2 - Band 2');

// Verify the initial state
await expect(page.locator('[data-testid="band-B"]')).toBeVisible();
await expect(page.locator('[data-testid="band-B"]')).toContainText('b3 - Band 3');
});
});
16 changes: 16 additions & 0 deletions __tests__/playwright/setup-msw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { test as base, expect } from '@playwright/test';
import { http } from 'msw';
import type { MockServiceWorker } from 'playwright-msw';
import { createWorkerFixture } from 'playwright-msw';

import {handlers} from '@/__mocks__/handlers';

const test = base.extend<{
worker: MockServiceWorker;
http: typeof http;
}>({
worker: createWorkerFixture(handlers),
http
});

export { expect, test };
38 changes: 38 additions & 0 deletions app/cog-viewer/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import AppLayout from '@/components/Layout';
import { Amplify } from 'aws-amplify';
import { withAuthenticator } from '@aws-amplify/ui-react';
import { config } from '@/utils/aws-exports';
import { SignInHeader } from '@/components/SignInHeader';
import { withConditionalAuthenticator } from "@/utils/withConditionalAuthenticator";


import dynamic from "next/dynamic";
import "leaflet/dist/leaflet.css";

// Dynamically load the COGOverlay component to prevent SSR issues
const COGViewer = dynamic(() => import('@/components/COGViewer'), {
ssr: false,
});

Amplify.configure({ ...config }, { ssr: true });


const Renders = function Renders() {

return (
<AppLayout>
<COGViewer/>
</AppLayout>
);
};

export default withConditionalAuthenticator(Renders, {
hideSignUp: true,
components: {
SignIn: {
Header: SignInHeader,
},
},
});
3 changes: 2 additions & 1 deletion app/create-ingest/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import IngestCreationForm from '@/components/IngestCreationForm';
import ErrorModal from '@/components/ErrorModal';
import SuccessModal from '@/components/SuccessModal';
import { Status } from '@/types/global';
import { withConditionalAuthenticator } from '@/utils/withConditionalAuthenticator';

const CreateIngest = function CreateIngest() {
const [status, setStatus] = useState<Status>('idle');
Expand Down Expand Up @@ -49,7 +50,7 @@ const CreateIngest = function CreateIngest() {
);
};

export default withAuthenticator(CreateIngest, {
export default withConditionalAuthenticator(CreateIngest, {
hideSignUp: true,
components: {
SignIn: {
Expand Down
3 changes: 2 additions & 1 deletion app/edit-ingest/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Status } from '@/types/global';
import { Endpoints } from '@octokit/types';
import ErrorModal from '@/components/ErrorModal';
import SuccessModal from '@/components/SuccessModal';
import { withConditionalAuthenticator } from '@/utils/withConditionalAuthenticator';

// Type definitions
type PullRequest = Endpoints['GET /repos/{owner}/{repo}/pulls']['response']['data'][number];
Expand Down Expand Up @@ -142,7 +143,7 @@ const EditIngest = function EditIngest() {
);
};

export default withAuthenticator(EditIngest, {
export default withConditionalAuthenticator(EditIngest, {
hideSignUp: true,
components: {
SignIn: {
Expand Down
3 changes: 3 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import './globals.css';
import React from 'react';
import { Inter } from 'next/font/google';
import { AntdRegistry } from '@ant-design/nextjs-registry';
import "@ant-design/v5-patch-for-react-19";
import "leaflet/dist/leaflet.css";


const inter = Inter({ subsets: ['latin'] });

Expand Down
Loading