Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 29 additions & 26 deletions __tests__/src/components/AttributionPanel.test.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,58 @@
/**
* @jest-environment-options { "resources": "usable" }
*/
import { render, screen, waitFor } from '@tests/utils/test-utils';
import { render, screen, waitFor } from "@tests/utils/test-utils";

import { AttributionPanel } from '../../../src/components/AttributionPanel';
import { AttributionPanel } from "../../../src/components/AttributionPanel";

/**
* Helper function to create a shallow wrapper around AttributionPanel
*/
function createWrapper(props) {
return render(
<AttributionPanel
id="xyz"
windowId="window"
{...props}
/>,
{ preloadedState: { companionWindows: { xyz: { content: 'attribution' } } } },
);
return render(<AttributionPanel id="xyz" windowId="window" {...props} />, {
preloadedState: { companionWindows: { xyz: { content: "attribution" } } },
});
}

describe('AttributionPanel', () => {
it('renders the required statement', () => {
describe("AttributionPanel", () => {
it("renders the required statement", () => {
const requiredStatement = [
{ label: 'required statement', values: ['must be shown'] },
{ label: "required statement", values: ["must be shown"] },
];
createWrapper({ requiredStatement });

expect(screen.getByText('required statement')).toBeInTheDocument();
expect(screen.getByText('must be shown')).toBeInTheDocument();
expect(screen.getByText("required statement")).toBeInTheDocument();
expect(screen.getByText("must be shown")).toBeInTheDocument();
});

it('renders the rights statement', () => {
createWrapper({ rights: ['http://example.com', 'http://stanford.edu'] });
it("renders the rights statement", () => {
createWrapper({ rights: ["http://example.com", "http://stanford.edu"] });

expect(screen.getByText('License')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'http://example.com' })).toHaveAttribute('href', 'http://example.com');
expect(screen.getByRole('link', { name: 'http://stanford.edu' })).toHaveAttribute('href', 'http://stanford.edu');
expect(screen.getByText("License")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "http://example.com" }),
).toHaveAttribute("href", "http://example.com");
expect(
screen.getByRole("link", { name: "http://stanford.edu" }),
).toHaveAttribute("href", "http://stanford.edu");
});

it('does not render the rights statement if it is empty', () => {
it("does not render the rights statement if it is empty", () => {
createWrapper({ rights: [] });
expect(screen.queryByText('License')).not.toBeInTheDocument();
expect(screen.queryByText("License")).not.toBeInTheDocument();
});

// Requires canvas to handle img loading.
it.skip('renders the manifest logo', async () => {
const manifestLogo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMDQmtBwADgwF/Op8FmAAAAABJRU5ErkJggg==';
it("renders the manifest logo", async () => {
const manifestLogo =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMDQmtBwADgwF/Op8FmAAAAABJRU5ErkJggg==";

const { container } = createWrapper({ manifestLogo });
await waitFor(() => { expect(container.querySelector('img')).toBeInTheDocument(); }); // eslint-disable-line testing-library/no-container, testing-library/no-node-access
await waitFor(() => {
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
expect(container.querySelector("img")).toBeInTheDocument();
});

expect(container.querySelector('img')).toHaveAttribute('src', manifestLogo); // eslint-disable-line testing-library/no-container, testing-library/no-node-access
expect(container.querySelector("img")).toHaveAttribute("src", manifestLogo); // eslint-disable-line testing-library/no-container, testing-library/no-node-access
});
});
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"react-dnd-touch-backend": "^16.0.0",
"react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",
"react-image": "^4.0.1",
"react-intersection-observer": "^10.0.0",
"react-mosaic-component2": "^7.0.0",
"react-redux": "^8.0.0 || ^9.0.0",
Expand Down
99 changes: 52 additions & 47 deletions src/components/AttributionPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import Skeleton from '@mui/material/Skeleton';
import { useTranslation } from 'react-i18next';
import { Img } from 'react-image';
import CompanionWindow from '../containers/CompanionWindow';
import { CompanionWindowSection } from './CompanionWindowSection';
import LabelValueMetadata from '../containers/LabelValueMetadata';
import ns from '../config/css-ns';
import { PluginHook } from './PluginHook';
import PropTypes from "prop-types";
import { styled } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import Skeleton from "@mui/material/Skeleton";
import { useTranslation } from "react-i18next";
import CompanionWindow from "../containers/CompanionWindow";
import { CompanionWindowSection } from "./CompanionWindowSection";
import LabelValueMetadata from "../containers/LabelValueMetadata";
import ns from "../config/css-ns";
import { PluginHook } from "./PluginHook";
import { ImageWithFallback } from "./ImageWithFallback";

const StyledLogo = styled(Img)(() => ({
maxWidth: '100%',
const StyledLogo = styled(ImageWithFallback)(() => ({
maxWidth: "100%",
}));

const StyledPlaceholder = styled(Skeleton)(({ theme }) => ({
Expand All @@ -35,42 +35,45 @@ export function AttributionPanel({

return (
<CompanionWindow
title={t('attributionTitle')}
paperClassName={ns('attribution-panel')}
title={t("attributionTitle")}
paperClassName={ns("attribution-panel")}
windowId={windowId}
id={id}
>
<CompanionWindowSection>
{ requiredStatement && (
<LabelValueMetadata labelValuePairs={requiredStatement} defaultLabel={t('attribution')} />
{requiredStatement && (
<LabelValueMetadata
labelValuePairs={requiredStatement}
defaultLabel={t("attribution")}
/>
)}
{rights && rights.length > 0 && (
<dl className={ns("label-value-metadata")}>
<Typography variant="subtitle2" component="dt">
{t("rights")}
</Typography>
{rights.map((v) => (
<Typography variant="body1" component="dd" key={v.toString()}>
<Link target="_blank" rel="noopener noreferrer" href={v}>
{v}
</Link>
</Typography>
))}
</dl>
)}
{
rights && rights.length > 0 && (
<dl className={ns('label-value-metadata')}>
<Typography variant="subtitle2" component="dt">{t('rights')}</Typography>
{ rights.map(v => (
<Typography variant="body1" component="dd" key={v.toString()}>
<Link target="_blank" rel="noopener noreferrer" href={v}>
{v}
</Link>
</Typography>
)) }
</dl>
)
}
</CompanionWindowSection>

{ manifestLogo && (
<CompanionWindowSection>
<StyledLogo
src={[manifestLogo]}
alt=""
role="presentation"
unloader={
<StyledPlaceholder variant="rectangular" height={60} width={60} />
}
/>
</CompanionWindowSection>
{manifestLogo && (
<CompanionWindowSection>
<StyledLogo
src={manifestLogo}
alt=""
role="presentation"
fallback={
<StyledPlaceholder variant="rectangular" height={60} width={60} />
}
/>
</CompanionWindowSection>
)}

<PluginHook targetName="AttributionPanel" {...pluginProps} />
Expand All @@ -81,10 +84,12 @@ export function AttributionPanel({
AttributionPanel.propTypes = {
id: PropTypes.string.isRequired,
manifestLogo: PropTypes.string,
requiredStatement: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string,
})),
requiredStatement: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string,
}),
),
rights: PropTypes.arrayOf(PropTypes.string),
windowId: PropTypes.string.isRequired,
};
24 changes: 24 additions & 0 deletions src/components/ImageWithFallback.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useState } from "react";
import PropTypes from "prop-types";

/**
* A plain img element that renders a fallback node when the image fails to load.
*/
export function ImageWithFallback({ alt, fallback, src, ...props }) {
const [hasError, setHasError] = useState(false);

if (hasError) return fallback;

return (
<img alt={alt} src={src} onError={() => setHasError(true)} {...props} />
);
}

ImageWithFallback.propTypes = {
/** Alt text for the underlying img element. */
alt: PropTypes.string.isRequired,
/** Node rendered in place of the image when it fails to load. */
fallback: PropTypes.node.isRequired,
/** URL of the image to display. */
src: PropTypes.string.isRequired,
};
Loading
Loading