mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
pkc-js's publishChallengeAnswers destructures { challengeAnswers } from its
argument, so passing a bare string[] left challengeAnswers undefined and
silently broke challenge submission (i.e. posting). Wrap answers in the object
form via a publishPublicationChallengeAnswers helper with error logging, and
align the ChallengePublication type with the real runtime signature.
692 lines
24 KiB
TypeScript
692 lines
24 KiB
TypeScript
import * as React from 'react';
|
|
import { createElement } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import ChallengeModal from '../challenge-modal';
|
|
import getShortAddress from '../../../lib/get-short-address';
|
|
import useTrustedBoardUrlPermissionsStore from '../../../stores/use-trusted-board-url-permissions-store';
|
|
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
|
|
|
const testState = vi.hoisted(() => ({
|
|
abandonCurrentChallengeMock: vi.fn().mockResolvedValue(undefined),
|
|
account: {
|
|
author: {
|
|
address: '0xabc123',
|
|
},
|
|
} as Record<string, any>,
|
|
challenges: [] as Array<{ challenge: any; id: number }>,
|
|
commentsByCid: {} as Record<string, { author?: { shortAddress?: string } }>,
|
|
isMobile: false,
|
|
publicationPreview: 'preview body',
|
|
publicationType: 'post',
|
|
removeChallengeMock: vi.fn(),
|
|
springStartMock: vi.fn(),
|
|
theme: 'dark',
|
|
votePreview: 'upvote',
|
|
}));
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string, options?: Record<string, unknown>) => {
|
|
if (key === 'challenge_counter') {
|
|
return `${options?.index}/${options?.total}`;
|
|
}
|
|
if (key === 'iframe_challenge_confirm') {
|
|
return `${options?.board} wants to open ${options?.site}.\n\nFor {{publicationType}}: {{excerpt}}`;
|
|
}
|
|
if (key === 'trusted_board_link_checkbox') {
|
|
return 'Always allow boards to open this website';
|
|
}
|
|
return key;
|
|
},
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
|
useAccount: () => testState.account,
|
|
useComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? testState.commentsByCid[commentCid] : undefined),
|
|
}));
|
|
|
|
vi.mock('../../../lib/utils/challenge-utils', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../../lib/utils/challenge-utils')>();
|
|
return {
|
|
...actual,
|
|
getPublicationPreview: () => testState.publicationPreview,
|
|
getPublicationType: () => testState.publicationType,
|
|
getVotePreview: () => testState.votePreview,
|
|
};
|
|
});
|
|
|
|
vi.mock('../../../hooks/use-is-mobile', () => ({
|
|
default: () => testState.isMobile,
|
|
}));
|
|
|
|
vi.mock('../../../hooks/use-theme', () => ({
|
|
default: () => [testState.theme],
|
|
}));
|
|
|
|
vi.mock('../../../stores/use-challenges-store', () => ({
|
|
default: () => ({
|
|
abandonCurrentChallenge: testState.abandonCurrentChallengeMock,
|
|
challenges: testState.challenges,
|
|
removeChallenge: testState.removeChallengeMock,
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@react-spring/web', async () => {
|
|
const React = await vi.importActual<typeof import('react')>('react');
|
|
const createSpringValue = (value: number) => ({
|
|
get: () => value,
|
|
to: (mapper: (input: number) => unknown) => mapper(value),
|
|
});
|
|
return {
|
|
animated: {
|
|
div: React.forwardRef(({ children, style, ...props }: any, ref) => createElement('div', { ...props, ref, style: { touchAction: style?.touchAction } }, children)),
|
|
},
|
|
useSpring: () => [
|
|
{
|
|
x: createSpringValue(120),
|
|
y: createSpringValue(60),
|
|
},
|
|
{ start: testState.springStartMock },
|
|
],
|
|
};
|
|
});
|
|
|
|
vi.mock('@use-gesture/react', () => ({
|
|
useDrag: () => () => ({}),
|
|
}));
|
|
|
|
let alertSpy: ReturnType<typeof vi.spyOn>;
|
|
let confirmSpy: ReturnType<typeof vi.spyOn>;
|
|
let container: HTMLDivElement;
|
|
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
let nextStoredChallengeId = 1;
|
|
let postMessageMock: ReturnType<typeof vi.fn>;
|
|
let root: Root;
|
|
|
|
const createPublication = (): Record<string, any> => ({
|
|
author: { displayName: 'Alice' },
|
|
content: 'Publication content',
|
|
link: 'https://example.com/link',
|
|
parentCid: 'parent-1',
|
|
publishChallengeAnswers: vi.fn(),
|
|
shortCommunityAddress: 'mu',
|
|
communityAddress: 'music-posting.eth',
|
|
title: 'Subject',
|
|
});
|
|
|
|
const createStoredChallenge = (challenge: any, publication = createPublication(), publicationTarget?: Record<string, unknown>) => ({
|
|
challenge: [{ challenges: Array.isArray(challenge) ? challenge : [challenge] }, publication, publicationTarget],
|
|
id: nextStoredChallengeId++,
|
|
});
|
|
|
|
const renderModal = async () => {
|
|
await act(async () => {
|
|
root.render(createElement(ChallengeModal));
|
|
});
|
|
await act(async () => {
|
|
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
});
|
|
};
|
|
|
|
const dispatchInput = async (element: HTMLInputElement, value: string) => {
|
|
await act(async () => {
|
|
const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
|
|
descriptor?.set?.call(element, value);
|
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
};
|
|
|
|
const clickButton = async (text: string) => {
|
|
const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === text);
|
|
await act(async () => {
|
|
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
});
|
|
};
|
|
|
|
describe('ChallengeModal', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
localStorage.clear();
|
|
useTrustedBoardUrlPermissionsStore.setState({ trustedOrigins: {} });
|
|
testState.abandonCurrentChallengeMock.mockReset().mockResolvedValue(undefined);
|
|
testState.account = {
|
|
author: {
|
|
address: '0xabc123',
|
|
},
|
|
};
|
|
testState.challenges = [];
|
|
testState.commentsByCid = {
|
|
'parent-1': {
|
|
author: {
|
|
shortAddress: '0xparent',
|
|
},
|
|
},
|
|
};
|
|
testState.isMobile = false;
|
|
testState.publicationPreview = 'preview body';
|
|
testState.publicationType = 'post';
|
|
testState.removeChallengeMock.mockReset();
|
|
testState.springStartMock.mockReset();
|
|
testState.theme = 'dark';
|
|
testState.votePreview = 'upvote';
|
|
alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined);
|
|
confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true);
|
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
postMessageMock = vi.fn();
|
|
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
configurable: true,
|
|
get: () => ({
|
|
postMessage: postMessageMock,
|
|
}),
|
|
});
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
alertSpy.mockRestore();
|
|
confirmSpy.mockRestore();
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
|
|
it('renders nothing when there are no queued challenges', async () => {
|
|
await renderModal();
|
|
expect(container.innerHTML).toBe('');
|
|
});
|
|
|
|
it('submits a text challenge answer on Enter and closes the modal', async () => {
|
|
const publication = createPublication();
|
|
testState.publicationType = 'reply';
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: '2 + 2',
|
|
type: 'text/plain',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
expect(container.textContent).toContain('Challenge for reply');
|
|
expect(container.textContent).toContain('1/1');
|
|
expect(container.querySelector('textarea')?.textContent ?? container.textContent).toContain('Publication content');
|
|
|
|
const input = container.querySelector<HTMLInputElement>('input[placeholder*="TYPE THE ANSWER HERE"]');
|
|
expect(input).not.toBeNull();
|
|
|
|
await dispatchInput(input as HTMLInputElement, '4');
|
|
await act(async () => {
|
|
input?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }));
|
|
});
|
|
|
|
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: ['4'] });
|
|
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('centers the mobile modal from the viewport instead of its static position', async () => {
|
|
testState.isMobile = true;
|
|
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 375 });
|
|
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 667 });
|
|
testState.publicationType = 'reply';
|
|
testState.challenges = [
|
|
createStoredChallenge({
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
}),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
const dialog = container.querySelector<HTMLElement>('[role="dialog"]');
|
|
expect(container.textContent).toContain('wants to open spamblocker.bitsocial.net.');
|
|
expect(dialog?.style.left).toBe('50%');
|
|
expect(dialog?.style.top).toBe('0px');
|
|
expect(dialog?.style.transform).toBe('translate3d(-50%, 134px, 0)');
|
|
});
|
|
|
|
it('redacts generated fortune BBCode from challenge publication details', async () => {
|
|
const publication = {
|
|
...createPublication(),
|
|
content: 'body[fortune color=#fd4d32]Excellent Luck[/fortune]',
|
|
};
|
|
testState.publicationPreview = 'body';
|
|
testState.publicationType = 'post';
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: '2 + 2',
|
|
type: 'text/plain',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(container.querySelector<HTMLTextAreaElement>('textarea')?.value).toBe('body');
|
|
expect(container.textContent).not.toContain('Excellent Luck');
|
|
expect(container.textContent).not.toContain('[fortune');
|
|
});
|
|
|
|
it('supports multi-step image challenges with next and previous navigation', async () => {
|
|
const publication = createPublication();
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
[
|
|
{
|
|
challenge: 'first answer',
|
|
type: 'text/plain',
|
|
},
|
|
{
|
|
challenge: 'YmFzZTY0LWltYWdl',
|
|
type: 'image/png',
|
|
},
|
|
],
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
expect(container.textContent).toContain('1/2');
|
|
|
|
const input = container.querySelector<HTMLInputElement>('input[placeholder*="TYPE THE ANSWER HERE"]');
|
|
await dispatchInput(input as HTMLInputElement, 'step one');
|
|
await act(async () => {
|
|
input?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' }));
|
|
});
|
|
|
|
expect(container.textContent).toContain('2/2');
|
|
expect(container.querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,YmFzZTY0LWltYWdl');
|
|
|
|
await clickButton('previous');
|
|
expect(container.textContent).toContain('1/2');
|
|
|
|
await clickButton('next');
|
|
await dispatchInput(container.querySelector<HTMLInputElement>('input[placeholder*="TYPE THE ANSWER HERE"]') as HTMLInputElement, 'step two');
|
|
await clickButton('submit');
|
|
|
|
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: ['step one', 'step two'] });
|
|
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('opens iframe challenges through the custom modal, injects the theme, and completes them', async () => {
|
|
const publication = createPublication();
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: 'https://mintpass.org/auth?user={userAddress}',
|
|
type: 'url/iframe',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(container.querySelector('#challenge-modal-title')?.textContent).toContain('Challenge for post');
|
|
expect(container.textContent).toContain('mintpass.org');
|
|
expect(container.textContent).toContain('mu wants to open mintpass.org.');
|
|
expect(container.textContent).not.toContain('For post:');
|
|
expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}');
|
|
expect(container.textContent).toContain('Open');
|
|
expect(container.textContent).toContain('Always allow boards to open this website');
|
|
expect(container.textContent).not.toContain('Always allow boards to open mintpass.org');
|
|
expect(container.textContent).not.toContain('Cancel');
|
|
const publicationFields = Array.from(container.querySelectorAll<HTMLInputElement>('input[type="text"]'));
|
|
expect(publicationFields.map((input) => input.value)).toEqual(['Alice', 'Subject', 'https://example.com/link']);
|
|
expect(publicationFields.every((input) => input.disabled)).toBe(true);
|
|
const publicationTextarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
|
expect(publicationTextarea?.value).toBe('Publication content');
|
|
expect(publicationTextarea?.disabled).toBe(true);
|
|
expect(container.querySelector('iframe')).toBeNull();
|
|
|
|
await clickButton('Open');
|
|
|
|
const iframe = container.querySelector('iframe');
|
|
expect(iframe).not.toBeNull();
|
|
expect(iframe?.getAttribute('src')).toContain('https://mintpass.org/auth?user=0xabc123&theme=dark');
|
|
expect(iframe?.getAttribute('sandbox')).toBe('allow-scripts allow-forms allow-popups allow-same-origin allow-top-navigation-by-user-activation');
|
|
|
|
await act(async () => {
|
|
iframe?.dispatchEvent(new Event('load', { bubbles: true }));
|
|
});
|
|
|
|
expect(postMessageMock).toHaveBeenCalledWith(
|
|
{
|
|
source: 'plebbit-5chan',
|
|
theme: 'dark',
|
|
type: 'plebbit-theme',
|
|
},
|
|
'https://mintpass.org',
|
|
);
|
|
|
|
const doneButton = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === 'Finish Challenge');
|
|
expect(doneButton?.getAttribute('aria-label')).toBe('Finish challenge');
|
|
|
|
await clickButton('Finish Challenge');
|
|
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: [''] });
|
|
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('remembers a trusted iframe origin when the checkbox is selected', async () => {
|
|
testState.challenges = [
|
|
createStoredChallenge({
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
}),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
const checkbox = container.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
|
await act(async () => {
|
|
checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
});
|
|
await clickButton('Open');
|
|
|
|
expect(useTrustedBoardUrlPermissionsStore.getState().isOriginTrusted('https://spamblocker.bitsocial.net')).toBe(true);
|
|
expect(useTrustedBoardUrlPermissionsStore.getState().getTrustedOrigins()).toEqual([
|
|
expect.objectContaining({
|
|
origin: 'https://spamblocker.bitsocial.net',
|
|
site: 'spamblocker.bitsocial.net',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('opens trusted iframe origins without asking again', async () => {
|
|
useTrustedBoardUrlPermissionsStore.getState().trustOrigin('https://spamblocker.bitsocial.net', 'spamblocker.bitsocial.net');
|
|
testState.challenges = [
|
|
createStoredChallenge({
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
}),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(container.textContent).not.toContain('wants to open');
|
|
expect(container.querySelector('iframe')?.getAttribute('src')).toContain('https://spamblocker.bitsocial.net/api/v1/iframe/session-123?theme=dark');
|
|
});
|
|
|
|
it('allows localhost http iframe challenges for local spam blocker testing', async () => {
|
|
const publication = createPublication();
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: 'http://localhost:3000/api/v1/iframe/session-123?foo=bar',
|
|
type: 'url/iframe',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(container.textContent).toContain('mu wants to open localhost:3000.');
|
|
expect(container.textContent).not.toContain('For post:');
|
|
expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}');
|
|
|
|
await clickButton('Open');
|
|
|
|
const iframe = container.querySelector('iframe');
|
|
expect(iframe).not.toBeNull();
|
|
expect(iframe?.getAttribute('src')).toContain('http://localhost:3000/api/v1/iframe/session-123?foo=bar&theme=dark');
|
|
|
|
await act(async () => {
|
|
iframe?.dispatchEvent(new Event('load', { bubbles: true }));
|
|
});
|
|
|
|
expect(postMessageMock).toHaveBeenCalledWith(
|
|
{
|
|
source: 'plebbit-5chan',
|
|
theme: 'dark',
|
|
type: 'plebbit-theme',
|
|
},
|
|
'http://localhost:3000',
|
|
);
|
|
});
|
|
|
|
it('auto-submits iframe challenges when the iframe posts a completion message', async () => {
|
|
const publication = createPublication();
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
await clickButton('Open');
|
|
|
|
await act(async () => {
|
|
window.dispatchEvent(
|
|
new MessageEvent('message', {
|
|
data: {
|
|
type: 'challengeAnswer',
|
|
challengeAnswers: [''],
|
|
sessionId: 'session-123',
|
|
},
|
|
origin: 'https://spamblocker.bitsocial.net',
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: [''] });
|
|
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('advances through multiple iframe challenges before publishing answers', async () => {
|
|
const publication = createPublication();
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
[
|
|
{
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
},
|
|
{
|
|
challenge: 'https://flags.5chan.app/iframe/session-flag',
|
|
type: 'url/iframe',
|
|
},
|
|
],
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
await clickButton('Open');
|
|
|
|
await act(async () => {
|
|
window.dispatchEvent(
|
|
new MessageEvent('message', {
|
|
data: {
|
|
type: 'challengeAnswer',
|
|
challengeAnswers: [''],
|
|
sessionId: 'session-123',
|
|
},
|
|
origin: 'https://spamblocker.bitsocial.net',
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(publication.publishChallengeAnswers).not.toHaveBeenCalled();
|
|
expect(testState.removeChallengeMock).not.toHaveBeenCalled();
|
|
expect(container.textContent).toContain('mu wants to open flags.5chan.app.');
|
|
|
|
await clickButton('Open');
|
|
|
|
await act(async () => {
|
|
window.dispatchEvent(
|
|
new MessageEvent('message', {
|
|
data: {
|
|
type: 'challengeAnswer',
|
|
challengeAnswers: [''],
|
|
sessionId: 'session-flag',
|
|
},
|
|
origin: 'https://flags.5chan.app',
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(publication.publishChallengeAnswers).toHaveBeenCalledWith({ challengeAnswers: ['', ''] });
|
|
expect(testState.removeChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('ignores iframe completion messages with the wrong session id', async () => {
|
|
const publication = createPublication();
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
await clickButton('Open');
|
|
|
|
await act(async () => {
|
|
window.dispatchEvent(
|
|
new MessageEvent('message', {
|
|
data: {
|
|
type: 'challengeAnswer',
|
|
challengeAnswers: [''],
|
|
sessionId: 'session-999',
|
|
},
|
|
origin: 'https://spamblocker.bitsocial.net',
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(publication.publishChallengeAnswers).not.toHaveBeenCalled();
|
|
expect(testState.removeChallengeMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses the shortened community address when shortCommunityAddress is unavailable', async () => {
|
|
const longCommunityAddress = '12D3KooWS6yKc5N7o6JcAYHZpaQwAwyh1VddYatarU75Se3HXEeD';
|
|
const publication = {
|
|
...createPublication(),
|
|
shortCommunityAddress: undefined,
|
|
communityAddress: longCommunityAddress,
|
|
};
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: 'http://localhost:3000/api/v1/iframe/session-123?foo=bar',
|
|
type: 'url/iframe',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(container.textContent).toContain(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000.`);
|
|
expect(container.textContent).not.toContain('For post:');
|
|
expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}');
|
|
expect(container.textContent).not.toContain(longCommunityAddress);
|
|
});
|
|
|
|
it('shows reply content in disabled fields instead of the iframe confirmation message', async () => {
|
|
const publication = {
|
|
...createPublication(),
|
|
content: '>>17\nA reply body with enough context',
|
|
title: '',
|
|
};
|
|
testState.publicationType = 'reply';
|
|
testState.challenges = [
|
|
createStoredChallenge(
|
|
{
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
},
|
|
publication,
|
|
),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(container.querySelector('#challenge-modal-title')?.textContent).toContain('Challenge for reply');
|
|
expect(container.textContent).toContain('mu wants to open spamblocker.bitsocial.net.');
|
|
expect(container.textContent).not.toContain('For reply:');
|
|
expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}');
|
|
expect(container.querySelector<HTMLTextAreaElement>('textarea')?.value).toBe('>>17\nA reply body with enough context');
|
|
});
|
|
|
|
it('abandons iframe challenges when the custom confirmation is closed', async () => {
|
|
testState.challenges = [
|
|
createStoredChallenge({
|
|
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
|
|
type: 'url/iframe',
|
|
}),
|
|
];
|
|
|
|
await renderModal();
|
|
await clickButton('close');
|
|
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
|
|
expect(container.querySelector('iframe')).toBeNull();
|
|
});
|
|
|
|
it('shows iframe challenge errors inline instead of relying on alert', async () => {
|
|
testState.account = { author: { address: '' } };
|
|
testState.challenges = [
|
|
createStoredChallenge({
|
|
challenge: 'https://mintpass.org/auth?user={userAddress}',
|
|
type: 'url/iframe',
|
|
}),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(alertSpy).not.toHaveBeenCalled();
|
|
expect(container.textContent).toContain('Error: Unable to load challenge without your address. Please sign in and try again.');
|
|
expect(container.textContent).not.toContain('Open');
|
|
expect(container.querySelector('iframe')).toBeNull();
|
|
|
|
await clickButton('close');
|
|
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('shows invalid iframe challenge errors inline and still responds to Escape', async () => {
|
|
testState.challenges = [
|
|
createStoredChallenge({
|
|
challenge: 'http://example.com/unsafe',
|
|
type: 'url/iframe',
|
|
}),
|
|
];
|
|
|
|
await renderModal();
|
|
|
|
expect(alertSpy).not.toHaveBeenCalled();
|
|
expect(confirmSpy).not.toHaveBeenCalled();
|
|
expect(container.textContent).toContain('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
|
|
expect(testState.abandonCurrentChallengeMock).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
});
|
|
|
|
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
|
|
});
|
|
});
|