> For the complete documentation index, see [llms.txt](https://docs.wellcomecollection.org/request-for-comments-rfcs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.wellcomecollection.org/request-for-comments-rfcs/086-item-viewer-refactor/refactoring-iiif-viewer-context-testing.md).

# IIIF Viewer Context Refactoring - Testing Guide

**Related:** See [refactoring-iiif-viewer-context.md](https://github.com/wellcomecollection/docs/blob/main/rfcs/086-item-viewer-refactor/refactoring-iiif-viewer-context.md) for the main refactoring plan.

This document contains detailed test examples for the IIIF Viewer context refactoring. All tests should use proper TypeScript types.

## Phase 1 Testing

### 1.1 Context Provider Unit Tests

**File:** `content/webapp/contexts/ItemViewerContextV2/ItemViewerContextV2.test.tsx`

```typescript
import { render, screen } from '@testing-library/react';
import type { FC } from 'react';

import ItemViewerContextV2, { ItemViewerContextV2Props } from './index';
import { mockDefaultContext } from './test-utils';

// Test component that displays context values
const ContextValueDisplay: FC = () => {
  const {
    currentCanvas,
    currentCanvasIndex,
    mainImageService,
    hasMultipleCanvases,
    isCurrentCanvasRestricted,
    isFirstCanvas,
    isLastCanvas,
    canNavigateNext,
    canNavigatePrevious,
    hasIiifImageService,
    isImageZoomable,
    isWorkBornDigital,
  } = ItemViewerContextV2._currentValue;

  return (
    <div>
      <div data-testid="currentCanvasIndex">{currentCanvasIndex}</div>
      <div data-testid="currentCanvasId">{currentCanvas?.id || 'none'}</div>
      <div data-testid="mainImageServiceId">{mainImageService['@id'] || 'none'}</div>
      <div data-testid="hasMultipleCanvases">{String(hasMultipleCanvases)}</div>
      <div data-testid="isCurrentCanvasRestricted">{String(isCurrentCanvasRestricted)}</div>
      <div data-testid="isFirstCanvas">{String(isFirstCanvas)}</div>
      <div data-testid="isLastCanvas">{String(isLastCanvas)}</div>
      <div data-testid="canNavigateNext">{String(canNavigateNext)}</div>
      <div data-testid="canNavigatePrevious">{String(canNavigatePrevious)}</div>
      <div data-testid="hasIiifImageService">{String(hasIiifImageService)}</div>
      <div data-testid="isImageZoomable">{String(isImageZoomable)}</div>
      <div data-testid="isWorkBornDigital">{String(isWorkBornDigital)}</div>
    </div>
  );
};

describe('ItemViewerContextV2 - Derived Canvas Data', () => {
  it('should calculate currentCanvasIndex from canvas query param', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      query: { canvas: 3, manifest: 1, page: 1, shouldScrollToCanvas: true, query: '' },
      currentCanvasIndex: 2, // canvas=3 to index 2
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('currentCanvasIndex')).toHaveTextContent('2');
  });

  it('should handle mainImageService with empty string fallback', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      currentCanvas: { id: 'canvas-1', imageServiceId: undefined },
      mainImageService: { '@id': '' },
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('mainImageServiceId')).toHaveTextContent('none');
  });

  it('should detect multiple canvases correctly', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      transformedManifest: {
        canvases: [
          { id: 'c1', label: 'Canvas 1' },
          { id: 'c2', label: 'Canvas 2' },
        ],
      },
      hasMultipleCanvases: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('hasMultipleCanvases')).toHaveTextContent('true');
  });

  it('should correctly identify restricted canvas', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      currentCanvas: {
        id: 'canvas-1',
        label: 'Canvas 1',
        accessConditions: [{ type: 'RestrictedAccess' }],
      },
      isCurrentCanvasRestricted: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('isCurrentCanvasRestricted')).toHaveTextContent('true');
  });

  it('should correctly calculate navigation booleans for first canvas', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      currentCanvasIndex: 0,
      transformedManifest: {
        canvases: [
          { id: 'c1', label: 'Canvas 1' },
          { id: 'c2', label: 'Canvas 2' },
          { id: 'c3', label: 'Canvas 3' },
        ],
      },
      hasMultipleCanvases: true,
      isFirstCanvas: true,
      isLastCanvas: false,
      canNavigateNext: true,
      canNavigatePrevious: false,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('isFirstCanvas')).toHaveTextContent('true');
    expect(screen.getByTestId('isLastCanvas')).toHaveTextContent('false');
    expect(screen.getByTestId('canNavigateNext')).toHaveTextContent('true');
    expect(screen.getByTestId('canNavigatePrevious')).toHaveTextContent('false');
  });

  it('should correctly calculate navigation booleans for last canvas', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      currentCanvasIndex: 2,
      transformedManifest: {
        canvases: [
          { id: 'c1', label: 'Canvas 1' },
          { id: 'c2', label: 'Canvas 2' },
          { id: 'c3', label: 'Canvas 3' },
        ],
      },
      hasMultipleCanvases: true,
      isFirstCanvas: false,
      isLastCanvas: true,
      canNavigateNext: false,
      canNavigatePrevious: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('isFirstCanvas')).toHaveTextContent('false');
    expect(screen.getByTestId('isLastCanvas')).toHaveTextContent('true');
    expect(screen.getByTestId('canNavigateNext')).toHaveTextContent('false');
    expect(screen.getByTestId('canNavigatePrevious')).toHaveTextContent('true');
  });

  it('should detect IIIF image service correctly', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      currentCanvas: {
        id: 'canvas-1',
        label: 'Canvas 1',
        imageServiceId: 'https://iiif.example.com/image/123',
      },
      hasIiifImageService: true,
      isImageZoomable: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('hasIiifImageService')).toHaveTextContent('true');
    expect(screen.getByTestId('isImageZoomable')).toHaveTextContent('true');
  });

  it('should detect born digital works', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      work: {
        id: 'work-1',
        title: 'Test Work',
        production: [{ type: 'BornDigital' }],
      },
      isWorkBornDigital: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ContextValueDisplay />
      </ItemViewerContextV2.Provider>
    );

    expect(screen.getByTestId('isWorkBornDigital')).toHaveTextContent('true');
  });
});
```

### 1.2 Mock Utilities with Types

**File:** `content/webapp/contexts/ItemViewerContextV2/test-utils.ts`

```typescript
import type { ItemViewerContextV2Props } from './index';
import type { TransformedManifest } from '@weco/content/types/manifest';
import type { WorkBasic, Work } from '@weco/content/services/wellcome/catalogue/types';
import type { CanvasRotatedImage } from '@weco/content/types/item-viewer';
import type { UiTree } from '@weco/content/views/pages/works/work/work.types';

export const mockDefaultContext: ItemViewerContextV2Props = {
  // EXISTING PROPS (unchanged)
  query: { 
    canvas: 1, 
    manifest: 1, 
    page: 1, 
    shouldScrollToCanvas: true, 
    query: '' 
  },
  work: { 
    id: 'test', 
    title: 'Test Work',
    alternativeTitles: [],
    description: undefined,
    languageId: undefined,
    thumbnail: undefined,
    referenceNumber: undefined,
    productionDates: [],
    archiveLabels: undefined,
    cardLabels: [],
    primaryContributorLabel: undefined,
    notes: [],
  } as WorkBasic & Pick<Work, 'description'>,
  transformedManifest: undefined as TransformedManifest | undefined,
  parentManifest: undefined,
  searchResults: null,
  setSearchResults: jest.fn(),
  accessToken: undefined,
  archiveTree: [] as UiTree,
  setArchiveTree: jest.fn(),
  canvasIndexById: {},
  
  // UI PROPS
  viewerRef: { current: null },
  mainAreaRef: { current: null },
  mainAreaWidth: 1000,
  mainAreaHeight: 500,
  gridVisible: false,
  setGridVisible: jest.fn(),
  isFullscreen: false,
  setIsFullscreen: jest.fn(),
  isDesktopSidebarActive: true,
  setIsDesktopSidebarActive: jest.fn(),
  isMobileSidebarActive: false,
  setIsMobileSidebarActive: jest.fn(),
  showZoomed: false,
  setShowZoomed: jest.fn(),
  showFullscreenControl: false,
  setShowFullscreenControl: jest.fn(),
  showControls: false,
  setShowControls: jest.fn(),
  rotatedImages: [] as CanvasRotatedImage[],
  setRotatedImages: jest.fn(),
  isResizing: false,
  errorHandler: undefined,
  hasOnlyRenderableImages: false,
  
  // NEW: DERIVED CANVAS DATA (Phase 1)
  currentCanvasIndex: 0,
  currentCanvas: undefined,
  mainImageService: { '@id': undefined },
  urlTemplate: undefined,
  imageUrl: undefined,
  
  // NEW: CANVAS-RELATED BOOLEANS
  hasMultipleCanvases: false,
  isCurrentCanvasRestricted: false,
  isFirstCanvas: true,
  isLastCanvas: true,
  canNavigateNext: false,
  canNavigatePrevious: false,
  
  // NEW: IMAGE SERVICE BOOLEANS
  hasIiifImageService: false,
  hasIiifImage: false,
  shouldUseIiifImageLocation: false,
  hasOnlyRenderableImages: false,
  isImageZoomable: false,
  
  // NEW: WORK TYPE BOOLEANS
  isWorkBornDigital: false,
  hasVideoContent: false,
  hasAudioContent: false,
  
  // NEW: DOWNLOAD BOOLEANS
  hasDownloadOptions: false,
  canDownloadCurrentCanvas: false,
};
```

### 1.3 Component Integration Tests

**File:** `content/webapp/views/pages/works/work/IIIFViewer/IIIFViewer.refactored.test.tsx`

```typescript
import { render, screen } from '@testing-library/react';
import { RouterContext } from 'next/dist/shared/lib/router-context';
import type { NextRouter } from 'next/router';

import IIIFViewerRefactored from './IIIFViewer.refactored';
import type { TransformedManifest } from '@weco/content/types/manifest';
import type { WorkBasic, Work } from '@weco/content/services/wellcome/catalogue/types';

const mockRouter: NextRouter = {
  pathname: '/works/[workId]/items',
  route: '/works/[workId]/items',
  query: { workId: 'test-work', canvas: '1' },
  asPath: '/works/test-work/items',
  basePath: '',
  isLocaleDomain: false,
  push: jest.fn(),
  replace: jest.fn(),
  reload: jest.fn(),
  back: jest.fn(),
  forward: jest.fn(),
  prefetch: jest.fn().mockResolvedValue(undefined),
  beforePopState: jest.fn(),
  events: {
    on: jest.fn(),
    off: jest.fn(),
    emit: jest.fn(),
  },
  isFallback: false,
  isReady: true,
  isPreview: false,
};

const mockWork: WorkBasic & Pick<Work, 'description'> = {
  id: 'test-work',
  title: 'Test Work',
  alternativeTitles: [],
  production: [],
  description: 'Test description',
};

const mockTransformedManifest: TransformedManifest = {
  id: 'test-manifest',
  label: 'Test Manifest',
  canvases: [
    { 
      id: 'canvas-1', 
      label: 'Canvas 1',
      width: 1000,
      height: 1000,
    },
  ],
};

describe('IIIFViewer.refactored - Context Integration', () => {
  it('should provide currentCanvas to child components', () => {
    render(
      <RouterContext.Provider value={mockRouter}>
        <IIIFViewerRefactored
          work={mockWork}
          transformedManifest={mockTransformedManifest}
          searchResults={null}
          setSearchResults={jest.fn()}
        />
      </RouterContext.Provider>
    );

    // Verify ViewerTopBar can access currentCanvas
    expect(screen.queryByTestId('viewer-top-bar')).toBeInTheDocument();
  });

  it('should provide navigation booleans to components', () => {
    const manifest: TransformedManifest = {
      ...mockTransformedManifest,
      canvases: [
        { id: 'c1', label: 'Canvas 1', width: 1000, height: 1000 },
        { id: 'c2', label: 'Canvas 2', width: 1000, height: 1000 },
        { id: 'c3', label: 'Canvas 3', width: 1000, height: 1000 },
      ],
    };

    render(
      <RouterContext.Provider value={{ ...mockRouter, query: { workId: 'test', canvas: '1' } }}>
        <IIIFViewerRefactored
          work={mockWork}
          transformedManifest={manifest}
          searchResults={null}
          setSearchResults={jest.fn()}
        />
      </RouterContext.Provider>
    );

    // First canvas - previous should be disabled
    const prevButton = screen.queryByLabelText(/previous/i);
    expect(prevButton).toBeDisabled();
    
    // Next should be enabled
    const nextButton = screen.queryByLabelText(/next/i);
    expect(nextButton).not.toBeDisabled();
  });

  it('should provide boolean flags for conditional rendering', () => {
    const restrictedManifest: TransformedManifest = {
      ...mockTransformedManifest,
      canvases: [{
        id: 'c1',
        label: 'Canvas 1',
        width: 1000,
        height: 1000,
        accessConditions: [{ type: 'RestrictedAccess' }],
      }],
    };

    render(
      <RouterContext.Provider value={mockRouter}>
        <IIIFViewerRefactored
          work={mockWork}
          transformedManifest={restrictedManifest}
          searchResults={null}
          setSearchResults={jest.fn()}
        />
      </RouterContext.Provider>
    );

    // Verify restricted badge appears when isCurrentCanvasRestricted is true
    expect(screen.queryByText(/restricted/i)).toBeInTheDocument();
  });
});
```

**File:** `content/webapp/views/pages/works/work/IIIFViewer/ViewerTopBar.refactored.test.tsx`

```typescript
import { render, screen } from '@testing-library/react';
import type { FC } from 'react';

import ItemViewerContextV2, { ItemViewerContextV2Props } from '@weco/content/contexts/ItemViewerContextV2';
import { mockDefaultContext } from '@weco/content/contexts/ItemViewerContextV2/test-utils';

import ViewerTopBarRefactored from './ViewerTopBar.refactored';
import type { TransformedCanvas } from '@weco/content/types/manifest';

describe('ViewerTopBar.refactored - Context Consumption', () => {
  it('should use currentCanvas from context instead of calculating it', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      currentCanvas: {
        id: 'test-canvas',
        label: 'Test Canvas',
        width: 1000,
        height: 1000,
      } as TransformedCanvas,
      hasDownloadOptions: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ViewerTopBarRefactored />
      </ItemViewerContextV2.Provider>
    );

    // Should render download button because hasDownloadOptions is true
    expect(screen.queryByRole('button', { name: /download/i })).toBeInTheDocument();
  });

  it('should use navigation booleans from context', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      canNavigateNext: false,
      canNavigatePrevious: true,
      isLastCanvas: true,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ViewerTopBarRefactored />
      </ItemViewerContextV2.Provider>
    );

    // Next button should be disabled when canNavigateNext is false
    const nextButton = screen.queryByLabelText(/next/i);
    expect(nextButton).toBeDisabled();

    // Previous button should be enabled when canNavigatePrevious is true
    const prevButton = screen.queryByLabelText(/previous/i);
    expect(prevButton).not.toBeDisabled();
  });

  it('should use isCurrentCanvasRestricted from context', () => {
    const contextValue: ItemViewerContextV2Props = {
      ...mockDefaultContext,
      isCurrentCanvasRestricted: true,
      canDownloadCurrentCanvas: false,
    };

    render(
      <ItemViewerContextV2.Provider value={contextValue}>
        <ViewerTopBarRefactored />
      </ItemViewerContextV2.Provider>
    );

    // Restricted badge should appear
    expect(screen.queryByText(/restricted/i)).toBeInTheDocument();
    
    // Download should be disabled for restricted content
    expect(screen.queryByRole('button', { name: /download/i })).toBeDisabled();
  });
});
```

## Manual Testing Checklist

See [14-testing-strategy.md](/request-for-comments-rfcs/086-item-viewer-refactor/14-testing-strategy.md) for the complete manual testing checklist.

***

**Last Updated:** 1 April 2026


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.wellcomecollection.org/request-for-comments-rfcs/086-item-viewer-refactor/refactoring-iiif-viewer-context-testing.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
