Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/Components/Header/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function Header({ title, lead, disableTranslate } = {}) {
<Row>
<Col style={{ textAlign: "center" }}>
{/* Offline/Online Indicator */}
<div
{/* <div
style={{
position: 'absolute',
display: 'none',
Expand All @@ -126,7 +126,7 @@ function Header({ title, lead, disableTranslate } = {}) {
role="status"
>
{online ? '🟢' : '🔴'}
</div>
</div> */}

<Button
href={
Expand Down
159 changes: 158 additions & 1 deletion src/Components/Header/header.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render, screen, fireEvent, cleanup, waitFor } from "@testing-library/re
import Header from "./Header";
import { norCalResistNumber } from "../Rights/content";
import * as utils from "../../utils";
import * as cacheUtils from "../../utils/cache";

describe("Header", () => {
let scrollIntoViewMock;
Expand Down Expand Up @@ -118,7 +119,27 @@ describe("Header", () => {
});

describe("Save button", () => {
test("shows caching prompt when app is already installed and cache not complete", async () => {
test("shows already cached message when app installed and cache complete", async () => {
matchMediaMock.mockReturnValue({
matches: true,
media: "(display-mode: standalone)",
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});

// Mock cache as complete
vi.spyOn(cacheUtils, 'isCached').mockReturnValue(true);

const { container } = render(<Header />);
const buttons = container.querySelectorAll('.share-bar button');
const saveButton = Array.from(buttons).find(btn => btn.textContent === 'Save');

fireEvent.click(saveButton);

expect(alertMock).toHaveBeenCalledWith("App is installed and offline resources are cached!");
});

test("shows offline warning when app is installed and offline", async () => {
matchMediaMock.mockReturnValue({
matches: true,
media: "(display-mode: standalone)",
Expand All @@ -141,6 +162,110 @@ describe("Header", () => {
});
});

test("shows loading spinner during caching", async () => {
matchMediaMock.mockReturnValue({
matches: true,
media: "(display-mode: standalone)",
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});

// Mock online state
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: true,
});

// Mock cacheResources to delay so we can see the spinner
let resolveCaching;
const cachingPromise = new Promise((resolve) => {
resolveCaching = resolve;
});
vi.spyOn(cacheUtils, 'cacheResources').mockReturnValue(cachingPromise);
vi.spyOn(cacheUtils, 'isCached').mockReturnValue(false);

const { container, rerender } = render(<Header />);
const buttons = container.querySelectorAll('.share-bar button');
const saveButton = Array.from(buttons).find(btn => btn.textContent === 'Save');

fireEvent.click(saveButton);

// Check for spinner during caching
await waitFor(() => {
const spinnerButton = Array.from(container.querySelectorAll('.share-bar button'))
.find(btn => btn.textContent.includes('Saving'));
expect(spinnerButton).toBeDefined();
expect(spinnerButton.querySelector('[role="status"]')).toBeDefined();
});

// Resolve the caching
resolveCaching({ success: true, cached: 10, failed: 0 });
await cachingPromise;
rerender(<Header />);
});

test("shows partial success alert when some resources fail to cache", async () => {
matchMediaMock.mockReturnValue({
matches: true,
media: "(display-mode: standalone)",
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});

// Mock online state
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: true,
});

// Mock cacheResources with partial failure
vi.spyOn(cacheUtils, 'cacheResources').mockResolvedValue({
success: false,
cached: 8,
failed: 2
});
vi.spyOn(cacheUtils, 'isCached').mockReturnValue(false);

const { container } = render(<Header />);
const buttons = container.querySelectorAll('.share-bar button');
const saveButton = Array.from(buttons).find(btn => btn.textContent === 'Save');

fireEvent.click(saveButton);

await waitFor(() => {
expect(alertMock).toHaveBeenCalledWith("Cached 8 resources. 2 failed to cache.");
});
});

test("shows error alert when caching completely fails", async () => {
matchMediaMock.mockReturnValue({
matches: true,
media: "(display-mode: standalone)",
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});

// Mock online state
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: true,
});

// Mock cacheResources to throw error
vi.spyOn(cacheUtils, 'cacheResources').mockRejectedValue(new Error("Network failed"));
vi.spyOn(cacheUtils, 'isCached').mockReturnValue(false);

const { container } = render(<Header />);
const buttons = container.querySelectorAll('.share-bar button');
const saveButton = Array.from(buttons).find(btn => btn.textContent === 'Save');

fireEvent.click(saveButton);

await waitFor(() => {
expect(alertMock).toHaveBeenCalledWith("Failed to cache resources: Network failed");
});
});

test("shows alert when beforeinstallprompt not available", () => {
const { container } = render(<Header />);
const buttons = container.querySelectorAll('.share-bar button');
Expand Down Expand Up @@ -247,6 +372,38 @@ describe("Header", () => {
const saveButtonAfter = Array.from(buttons).find(btn => btn.textContent === 'Save');
expect(saveButtonAfter).toBeDefined();
});

test("handles install prompt without userChoice property", () => {
const mockPrompt = vi.fn();
const mockDeferredPrompt = {
prompt: mockPrompt,
// No userChoice property
};

const { rerender, container } = render(<Header />);
const event = new Event("beforeinstallprompt");
event.preventDefault = vi.fn();
Object.defineProperty(event, "prompt", {
value: mockPrompt,
});
// Don't set userChoice
window.dispatchEvent(event);

rerender(<Header />);

let buttons = container.querySelectorAll('.share-bar button');
const saveButton = Array.from(buttons).find(btn => btn.textContent === 'Save');
fireEvent.click(saveButton);

expect(mockPrompt).toHaveBeenCalled();

// Button should be hidden after click (then re-shown due to no userChoice)
rerender(<Header />);
buttons = container.querySelectorAll('.share-bar button');
const saveButtonAfter = Array.from(buttons).find(btn => btn.textContent === 'Save');
// Without userChoice, button gets re-shown
expect(saveButtonAfter).toBeDefined();
});
});

describe("Share button", () => {
Expand Down
1 change: 1 addition & 0 deletions src/Components/Resources/ResourceModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function ResourceModal({ showModal, modalContent }) {
</p>

<Button
as="a"
variant="primary-outline"
href={link.url}
target="_blank"
Expand Down
Loading